ПутешеÑтвие через вызов P/Invoke
(Russian translation from English by Maxim Voloshin)
Ð¡ÐµÐ³Ð¾Ð´Ð½Ñ Ð¼Ñ‹ от отправимÑÑ Ñ‡ÐµÑ€ÐµÐ· вÑе, что ÑлучаетÑÑ, когда Ð’Ñ‹ делаете вызов P/Invoke. Мы увидим как загружаютÑÑ Ð½Ð°Ñ‚Ð¸Ð²Ð½Ñ‹Ðµ библиотеки и как работает маршалинг. Мы коÑнемÑÑ ÑÐ¾Ð³Ð»Ð°ÑˆÐµÐ½Ð¸Ñ Ð¾ вызовах и кодировки Ñимволов. И, наконец , мы лучше поймем, что проиÑходит когда мы делаем вызов в нативный код.
Ðачнем Ñ Ð¿Ñ€Ð¸Ð¼ÐµÑ€Ð° из недавней Ñтатьи, давайте поÑмотрим на типичную функцию, которую C# вызывает в нативном (C в данном Ñлучае) коде. Ðто один из доÑтаточно Ñ‚Ñжелых вызовов, дающий SQLite команду выполнить запроÑ:
public static ReturnCode Execute(Pointer<Database> database, string sql) { return PInvoke.sqlite3_exec( database.Ptr, sql, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); }
PInvoke.sqlite3_exec
Ñто extern
функциÑ, Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð½Ð°Ñ Ñледующим образом:
[DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern ReturnCode sqlite3_exec( IntPtr pDb, string sql, IntPtr callback, IntPtr userdata, IntPtr errmsg);
Теперь выберем IL2CPP в качеÑтве Ñкриптового бекенда и Ñоберем билд Ð´Ð»Ñ macOS на Unity 2018.3.1f1. ПоÑмотрите в папку MyProject_macOS_BackUpThisFolder_ButDontShipItWithYourGame
, там находитÑÑ Ð¿Ð¾Ð´Ð¿Ð°Ð¿ÐºÐ° il2cppOutput
Ñ Ð±Ð¾Ð»ÑŒÑˆÐ¸Ð¼ количеÑтвом .cpp
файлов. Один из них Ñодержит наш код, Ñто Bulk_Assembly-CSharp_0.cpp
, давайте откроем его и прокрутим вниз до Ñтроки:
// Sqlite/ReturnCode Sqlite::Execute(Sqlite/Pointer`1<Sqlite/Database>,System.String)
Далее идет Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ð½Ð° C++, которую IL2CPP Ñкомпилировал из кода на C#. Вот как она выглÑдит:
extern "C" IL2CPP_METHOD_ATTR int32_t Sqlite_Execute_mD47D78DB934E6BA9E8C537F1A6BC6C2F5B5F16D8 (Pointer_1_tA8033EA1406105F61E1B6D9BEB34FD0C2AC13CE3 ___database0, String_t* ___sql1, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) il2cpp_codegen_initialize_method (Sqlite_Execute_mD47D78DB934E6BA9E8C537F1A6BC6C2F5B5F16D8_MetadataUsageId); s_Il2CppMethodInitialized = true; Pointer_1_tA8033EA1406105F61E1B6D9BEB34FD0C2AC13CE3 L_0 = ___database0; intptr_t L_1 = L_0.get_Ptr_0(); String_t* L_2 = ___sql1; int32_t L_3 = PInvoke_sqlite3_exec_m41306BF2F0AF2643E884D0594432D388B61C4974((intptr_t)L_1, L_2, (intptr_t)(0), (intptr_t)(0), (intptr_t)(0), /*Ñкрытый аргумент*/NULL); return L_3; }
ЗдеÑÑŒ мы видим обычную инициализацию и поÑледующий вызов PInvoke_sqlite3_exec_m41306BF2F0AF2643E884D0594432D388B61C4974
. Ð’ качеÑтве аргументов передаетÑÑ Ptr
поле Ñтруктуры Pointer
и SQL команда string
. Пока что Ñто абÑолютно нормальный C++ код Ð´Ð»Ñ C# функции, который вызывает другую функцию.
Следующий шаг – проÑледовать за вызовом extern
функции PInvoke_sqlite3_exec_m41306BF2F0AF2643E884D0594432D388B61C4974
. Она может быть найдена в том же файле.
extern "C" IL2CPP_METHOD_ATTR int32_t PInvoke_sqlite3_exec_m41306BF2F0AF2643E884D0594432D388B61C4974 (intptr_t ___pDb0, String_t* ___sql1, intptr_t ___callback2, intptr_t ___userdata3, intptr_t ___errmsg4, const RuntimeMethod* method) { typedef int32_t (DEFAULT_CALL *PInvokeFunc) (intptr_t, char*, intptr_t, intptr_t, intptr_t); static PInvokeFunc il2cppPInvokeFunc; if (il2cppPInvokeFunc == NULL) int parameterSize = sizeof(intptr_t) + sizeof(char*) + sizeof(intptr_t) + sizeof(intptr_t) + sizeof(intptr_t); il2cppPInvokeFunc = il2cpp_codegen_resolve_pinvoke<PInvokeFunc>(IL2CPP_NATIVE_STRING("sqlite3"), "sqlite3_exec", IL2CPP_CALL_C, CHARSET_NOT_SPECIFIED, parameterSize, false); if (il2cppPInvokeFunc == NULL) IL2CPP_RAISE_MANAGED_EXCEPTION(il2cpp_codegen_get_not_supported_exception("Unable to find method for p/invoke: 'sqlite3_exec'"), NULL, NULL); // Маршалинг параметров '___sql1' в нативное предÑтавление char* ____sql1_marshaled = NULL; ____sql1_marshaled = il2cpp_codegen_marshal_string(___sql1); // Вызов нативной функции int32_t returnValue = il2cppPInvokeFunc(___pDb0, ____sql1_marshaled, ___callback2, ___userdata3, ___errmsg4); // ОчиÑтка параметров '___sql1' il2cpp_codegen_marshal_free(____sql1_marshaled); ____sql1_marshaled = NULL; return returnValue; }
Ð’ Ñамом начале мы видим static
переменную PInvokeFunc
. В C++, локальные переменные static
ÑохранÑетÑÑ Ð¾Ñ‚ вызова к вызову, а не находитÑÑ Ð² Ñтеке. По ÑущеÑтву, Ñто Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ð°Ñ Ð¿ÐµÑ€ÐµÐ¼ÐµÐ½Ð½Ð°Ñ, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ Ð¼Ð¾Ð¶ÐµÑ‚ быть доÑтупна только из функции. ПредполагаетÑÑ, что при первом вызове Ñта Ð¿ÐµÑ€ÐµÐ¼ÐµÐ½Ð½Ð°Ñ Ñ€Ð°Ð²Ð½Ð° null и if
блок выполнÑетÑÑ Ñ‡Ñ‚Ð¾Ð±Ñ‹ уÑтановить ее значение. Ðто такой же подход, как и выполнение некоторого кода Ð´Ð»Ñ Ð¸Ð½Ð¸Ñ†Ð¸Ð°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ð¸ данных перед первым вызовом функции.
ОÑÐ½Ð¾Ð²Ð½Ð°Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ð° заключаетÑÑ Ð² вызове il2cpp_codegen_resolve_pinvoke
Ñ Ñ€Ð°Ð·Ð»Ð¸Ñ‡Ð½Ñ‹Ð¼Ð¸ параметрами Ð´Ð»Ñ ÑƒÐºÐ°Ð·Ð°Ð½Ð¸Ñ ÐºÐ°Ðº должен быть извлечен указатель на нативную функцию. Первый параметр Ñто Ð¸Ð¼Ñ Ð½Ð°Ñ‚Ð¸Ð²Ð½Ð¾Ð¹ библиотеки: "sqlite3"
, в данном Ñлучае. Строка Ñ Ð¸Ð¼ÐµÐ½ÐµÐ¼ обернута макроÑом IL2CPP_NATIVE_STRING
. Чтобы увидеть его определение, откройте /path/to/Unity/installation/Contents/il2cpp/libil2cpp/il2cpp-api-types.h
. Ðтот путь может немного отличатьÑÑ ÐµÑли вы иÑпользуете не macOS. Определение макроÑа:
#if _MSC_VER typedef wchar_t Il2CppNativeChar; #define IL2CPP_NATIVE_STRING(str) L##str #else typedef char Il2CppNativeChar; #define IL2CPP_NATIVE_STRING(str) str #endif
Так как мы не иÑпользуем Windows (Ñ‚.е. _MSC_VER
не определен), Ñтот Ð¼Ð°ÐºÑ€Ð¾Ñ Ð½Ðµ имеет никакого Ñффекта.
Следующий параметр Ð¸Ð¼Ñ Ñ„ÑƒÐ½ÐºÑ†Ð¸Ð¸: "sqlite3_exec"
.
Третий, Ñто тип “ÑÐ¾Ð³Ð»Ð°ÑˆÐµÐ½Ð¸Ñ Ð¾ вызовахâ€, Ñквивалентный в C# перечиÑлению CallingConvention.Cdecl
: IL2CPP_CALL_C
.
Точно так же, четвертый параметр, Ñто тип кодировки Ñимволов в Ñтроке CHARSET_NOT_SPECIFIED
. Ð”Ð»Ñ Ð½ÐµÐ³Ð¾ не ÑущеÑтвует Ñквивалента в C#, но по умолчанию Ñто CharSet.Ansi
.
ПÑтый параметр предÑтавлÑет Ñобой Ñумму размеров параметров, которые принимает Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ð² нативном коде. Обратите внимание, что поÑле маршалинга иÑпользуетÑÑ Ñ€Ð°Ð·Ð¼ÐµÑ€ C char*
, вмеÑто C# string
.
И, наконец, шеÑтой параметр уточнÑет Ñтоит ли иÑпользовать иÑкажение имени. ИÑкажение выполнÑетÑÑ ÐºÐ¾Ð¼Ð¿Ð¸Ð»Ñтором Ð´Ð»Ñ ÑÐ¾Ð±Ð»ÑŽÐ´ÐµÐ½Ð¸Ñ ÑƒÑловий уникальноÑти имен функций C++. Ðто позволÑет реализовать, например, перегрузки, когда вÑе функции имеют одинаковое Ð¸Ð¼Ñ Ð² C++, но по-прежнему уникальные имена Ð´Ð»Ñ Ñ†ÐµÐ»ÐµÐ¹ привÑзки к таким Ñзыкам как C#.
Теперь, Ð¿Ð¾Ð½Ð¸Ð¼Ð°Ñ Ñти параметры, поÑмотрим на функцию il2cpp_codegen_resolve_pinvoke
. Она раÑполагаетÑÑ Ð² файле /path/to/Unity/installation/Contents/il2cpp/libil2cpp/vm/il2cpp-codegen-il2cpp.h
.
template<typename FunctionPointerType, size_t dynamicLibraryLength, size_t entryPointLength> inline FunctionPointerType il2cpp_codegen_resolve_pinvoke(const Il2CppNativeChar(&nativeDynamicLibrary)[dynamicLibraryLength], const char(&entryPoint)[entryPointLength], Il2CppCallConvention callingConvention, Il2CppCharSet charSet, int parameterSize, bool isNoMangle) { const PInvokeArguments pinvokeArgs = il2cpp::utils::StringView<Il2CppNativeChar>(nativeDynamicLibrary), il2cpp::utils::StringView<char>(entryPoint), callingConvention, charSet, parameterSize, isNoMangle }; return reinterpret_cast<FunctionPointerType>(il2cpp::vm::PlatformInvoke::Resolve(pinvokeArgs)); }
Ðта Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ð½Ð° Ñамом деле оборачивает вÑе параметры в PInvokeArguments
и передает их в il2cpp::vm::PlatformInvoke::Resolve
. При Ñтом она Ñоздает ÑкземплÑры клаÑÑа StringView
Ð´Ð»Ñ Ð¸Ð¼ÐµÐ½ библиотеки и нативной функции. Ðтот клаÑÑ Ð¼Ð¾Ð¶ÐµÑ‚ быть найден в /path/to/Unity/installation/Contents/il2cpp/libil2cpp/utils/StringView.h
. По большому Ñчету Ñто обертка Ð´Ð»Ñ ÑƒÐºÐ°Ð·Ð°Ñ‚ÐµÐ»Ñ Ð½Ð° первый Ñимвол Ñтроки и Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ¼ÐµÐ½Ð½Ð¾Ð¹ Ñо значением ее длины. Кроме того, он Ñодержит удобные функции Ð´Ð»Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ñ‹ Ñо Ñтроками. Вот выдержка из его начала:
template<typename CharType> class StringView { private: const CharType* m_String; size_t m_Length; // Intended to only be used by Empty() inline StringView() : m_String(NULL), m_Length(0) public: template<size_t Length> inline StringView(const CharType(&str)[Length]) : m_String(str), m_Length(Length - 1) };
Шаблонный конÑтруктор иÑпользует тот факт, что Ñтрока в C++ Ñто маÑÑив Ñимволов и его длина извеÑтна во Ð²Ñ€ÐµÐ¼Ñ ÐºÐ¾Ð¼Ð¿Ð¸Ð»Ñции, таким образом нет необходимоÑти вызывать strlen
или идти по вÑем Ñимволам пока не будет доÑтигнут Ñимвол конца Ñтроки NUL
.
Теперь давайте перейдем к функции il2cpp::vm::PlatformInvoke::Resolve
. Ее можно найти в /path/to/Unity/installation/Contents/il2cpp/libil2cpp/vm/PlatformInvoke.cpp
и выглÑдит она вот так:
Il2CppMethodPointer PlatformInvoke::Resolve(const PInvokeArguments& pinvokeArgs) { // Перед выполнением P/Invoke, проверьте еÑÑ‚ÑŒ ли он в хардкод-ÑпиÑке "извеÑтных P/Invoke", который отличаетÑÑ Ð´Ð»Ñ ÐºÐ°Ð¶Ð´Ð¾Ð¹ платформы. // Ðто решает неÑколько разных проблем при вызове P/Invoking в нативные ÑиÑтемные библиотеки из mscorlib.dll. // Ðекоторые платформы, например, UWP, не позволÑÑ‚ загрузить ÑиÑтемные библиотеки динамичеÑки во Ð²Ñ€ÐµÐ¼Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ. // Ðа других платформах (ÐЕ СТОИТ ИХ ÐÐЗЫВÐТЬ :O) функции, которые mscorlib.dll хочет вызвать через P/Invoke, // СущеÑтвуют в других ÑиÑтемных библиотеках, нежели указано в DllImport атрибуте. Il2CppMethodPointer function = os::LibraryLoader::GetHardcodedPInvokeDependencyFunctionPointer(pinvokeArgs.moduleName, pinvokeArgs.entryPoint); if (function != NULL) return function; void* dynamicLibrary = NULL; if (utils::VmStringUtils::CaseSensitiveEquals(il2cpp::utils::StringUtils::NativeStringToUtf8(pinvokeArgs.moduleName.Str()).c_str(), "__InternalDynamic")) dynamicLibrary = LibraryLoader::LoadLibrary(il2cpp::utils::StringView<Il2CppNativeChar>::Empty()); else dynamicLibrary = LibraryLoader::LoadLibrary(pinvokeArgs.moduleName); if (dynamicLibrary == NULL) std::basic_string<Il2CppNativeChar> message; message += IL2CPP_NATIVE_STRING("Unable to load DLL '"); message += pinvokeArgs.moduleName.Str(); message += IL2CPP_NATIVE_STRING("': The specified module could not be found."); Exception::Raise(Exception::GetDllNotFoundException(il2cpp::utils::StringUtils::NativeStringToUtf8(message).c_str())); function = os::LibraryLoader::GetFunctionPointer(dynamicLibrary, pinvokeArgs); if (function == NULL) std::basic_string<Il2CppNativeChar> message; message += IL2CPP_NATIVE_STRING("Unable to find an entry point named '"); message += il2cpp::utils::StringUtils::Utf8ToNativeString(pinvokeArgs.entryPoint.Str()); message += IL2CPP_NATIVE_STRING("' in '"); message += pinvokeArgs.moduleName.Str(); message += IL2CPP_NATIVE_STRING("'."); Exception::Raise(Exception::GetEntryPointNotFoundException(il2cpp::utils::StringUtils::NativeStringToUtf8(message).c_str())); return function; }
Ð’ Ñамом начале вызываетÑÑ os::LibraryLoader::GetHardcodedPInvokeDependencyFunctionPointer
. Ð’ macOS она находитÑÑ Ð² /path/to/Unity/installation/Contents/il2cpp/libil2cpp/os/Posix/LibraryLoader.cpp
и выглÑдит так:
Il2CppMethodPointer LibraryLoader::GetHardcodedPInvokeDependencyFunctionPointer(const il2cpp::utils::StringView<Il2CppNativeChar>& nativeDynamicLibrary, const il2cpp::utils::StringView<char>& entryPoint) { return NULL; }
Похоже, что Ð´Ð»Ñ macOS нет захардкоженных функций, и Ñтот вызов вÑегда возвращает null и, Ñледовательно, уÑловие if (function != NULL)
никогда не выполнитÑÑ.
Далее идет вызов utils::VmStringUtils::CaseSensitiveEquals
Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð²ÐµÑ€ÐºÐ¸ равно ли moduleName
(Ð¸Ð¼Ñ Ð½Ð°Ñ‚Ð¸Ð²Ð½Ð¾Ð¹ библиотеки) значению "__InternalDynamic"
. Ðто проÑто Ñравнение Ñтрок и мы не будем раÑÑматривать его детально. Ð’ данном Ñлучае, мы не иÑпользовали "__InternalDynamic"
, поÑтому уÑловие не выполнитÑÑ Ð¸ будет вызван LibraryLoader::LoadLibrary(pinvokeArgs.moduleName)
.
LibraryLoader::LoadLibrary
также находитÑÑ Ð² /path/to/Unity/installation/Contents/il2cpp/libil2cpp/vm/LibraryLoader.cpp
:
void* LibraryLoader::LoadLibrary(il2cpp::utils::StringView<Il2CppNativeChar> nativeDynamicLibrary) { if (s_FindPluginCallback) StringViewAsNullTerminatedStringOf(Il2CppNativeChar, nativeDynamicLibrary, libraryName); const Il2CppNativeChar* modifiedLibraryName = s_FindPluginCallback(libraryName); if (modifiedLibraryName != libraryName) utils::StringView<Il2CppNativeChar> modifiedDynamicLibrary(modifiedLibraryName, utils::StringUtils::StrLen(modifiedLibraryName)); return os::LibraryLoader::LoadDynamicLibrary(modifiedDynamicLibrary); return os::LibraryLoader::LoadDynamicLibrary(nativeDynamicLibrary); }
ÐŸÐµÑ€Ð²Ð°Ñ Ñтрока проверÑет равна ли нулю Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ð°Ñ Ð¿ÐµÑ€ÐµÐ¼ÐµÐ½Ð½Ð°Ñ
s_FindPluginCallback
, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ Ñ€Ð°Ñположена прÑмо перед функцией:
static Il2CppSetFindPlugInCallback s_FindPluginCallback = NULL;
Ðто указатель на функцию, которому может быть не приÑвоено значение. Похоже, что в macOS ему никогда ничего не приÑваиваетÑÑ. Ðо еÑли он валиден, Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ð²Ñ‹Ð·Ñ‹Ð²Ð°ÐµÑ‚ÑÑ, чтобы получить измененную верÑию имени библиотеки. Ð’ любом Ñлучае в конце концов вызываетÑÑ os::LibraryLoader::LoadDynamicLibrary
. В macOS ее можно увидеть в файле /path/to/Unity/installation/Contents/il2cpp/libil2cpp/os/Posix/LibraryLoader.cpp
:
void* LibraryLoader::LoadDynamicLibrary(const utils::StringView<Il2CppNativeChar>& nativeDynamicLibrary) { return LoadDynamicLibrary(nativeDynamicLibrary, RTLD_LAZY); }
Ðто только перегрузка той же функции Ñ Ð´Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ñ‹Ð¼ параметром: RTLD_LAZY
. Ðтот параметр означает, что Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ñ ÑƒÐºÐ°Ð·Ð°Ð½Ð½Ñ‹Ð¼ именем ищетÑÑ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ в библиотеке, иÑпользуемой в данный момент. Что каÑаетÑÑ Ð¿ÐµÑ€ÐµÐ³Ñ€ÑƒÐ·ÐºÐ¸, она находитÑÑ Ð´Ð°Ð»ÐµÐµ в том же файле:
void* LibraryLoader::LoadDynamicLibrary(const utils::StringView<Il2CppNativeChar>& nativeDynamicLibrary, int flags) { #ifdef VERBOSE_OUTPUT printf("Attempting to load dynamic library: %sn", nativeDynamicLibrary.Str()); #endif if (nativeDynamicLibrary.IsEmpty()) return LoadLibraryWithName(NULL, flags); StringViewAsNullTerminatedStringOf(char, nativeDynamicLibrary, libraryName); void* handle = LoadLibraryWithName(libraryName, flags); if (handle == NULL) handle = CheckLibraryVariations(libraryName, flags); if (handle == NULL) size_t lengthWithoutDotDll = nativeDynamicLibrary.Length() - 4; if (strncmp(libraryName + lengthWithoutDotDll, ".dll", 4) == 0) char* nativeDynamicLibraryWithoutExtension = static_cast<char*>(alloca((lengthWithoutDotDll + 1) * sizeof(char))); memcpy(nativeDynamicLibraryWithoutExtension, libraryName, lengthWithoutDotDll); nativeDynamicLibraryWithoutExtension[lengthWithoutDotDll] = 0; handle = CheckLibraryVariations(nativeDynamicLibraryWithoutExtension, flags); os::FastAutoLock lock(&s_NativeHandlesOpenMutex); if (handle != NULL) s_NativeHandlesOpen.insert(handle); return handle; }
УÑловие if (nativeDynamicLibrary.IsEmpty())
никогда не выполнитÑÑ Ñ‚.к. мы никогда не передаем пуÑÑ‚Ñ‹Ñ… Ñтрок. Далее вызываетÑÑ LoadLibraryWithName
, раÑполагающаÑÑÑ Ð² том же Ñамом файле:
static void* LoadLibraryWithName(const char* name, int flags) { #ifdef VERBOSE_OUTPUT printf("Trying name: %sn", name); #endif void* handle = NULL; #if IL2CPP_TARGET_IOS std::string dirName; if (utils::Environment::GetNumMainArgs() > 0) std::string main = utils::StringUtils::Utf16ToUtf8(utils::Environment::GetMainArgs()[0]); dirName = utils::PathUtils::DirectoryName(main); std::string libPath = utils::StringUtils::Printf("%s/%s", dirName.c_str(), name); handle = dlopen(libPath.c_str(), flags); // Возврат к иÑпользованию имени. Ðто может быть ÑиÑÑ‚ÐµÐ¼Ð½Ð°Ñ dylib. if (handle == NULL) handle = dlopen(name, flags); #else handle = dlopen(name, flags); #endif if (handle != NULL) return handle; #ifdef VERBOSE_OUTPUT printf("Error: %sn", dlerror()); #endif return NULL; }
Так как мы не иÑпользуем iOS, по Ñути Ñта Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ð²Ñ‹Ð·Ñ‹Ð²Ð°ÐµÑ‚ dlopen
, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ Ð³Ð¾Ð²Ð¾Ñ€Ð¸Ñ‚ macOS открыть нативную библиотеку. ПредполагаÑ, что открытие уÑпешно, Ð²Ñ‹Ð·Ñ‹Ð²Ð°ÑŽÑ‰Ð°Ñ Ñторона пропуÑкает вÑе проверки нулевого деÑкриптора, и мы проÑто выполним чаÑÑ‚ÑŒ в конце функции, где проиÑходит блокировка, и деÑкриптор передаетÑÑ Ð² s_NativeHandlesOpen
.
Теперь мы можем вернутьÑÑ Ðº PlatformInvoke::Resolve
и обратить Ñвое внимание на функцию os::LibraryLoader::GetFunctionPointer
, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ Ð²Ñ‹Ð·Ñ‹Ð²Ð°ÐµÑ‚ÑÑ Ñ Ñ€ÐµÐ·ÑƒÐ»ÑŒÑ‚Ð°Ñ‚Ð¾Ð¼ Ð¾Ñ‚ÐºÑ€Ñ‹Ñ‚Ð¸Ñ Ð±Ð¸Ð±Ð»Ð¸Ð¾Ñ‚ÐµÐºÐ¸. ÐаходитÑÑ Ñ‚Ð°Ð¼ же:
Il2CppMethodPointer LibraryLoader::GetFunctionPointer(void* dynamicLibrary, const PInvokeArguments& pinvokeArgs) { StringViewAsNullTerminatedStringOf(char, pinvokeArgs.entryPoint, entryPoint); if (pinvokeArgs.isNoMangle) return reinterpret_cast<Il2CppMethodPointer>(dlsym(dynamicLibrary, entryPoint)); const size_t kBufferOverhead = 10; void* functionPtr = NULL; size_t originalFuncNameLength = strlen(entryPoint) + 1; std::string functionName; functionName.resize(originalFuncNameLength + kBufferOverhead + 1); // ИндекÑÐ°Ñ†Ð¸Ñ Ð½Ð°Ñ‡Ð¸Ð½Ð°ÐµÑ‚ÑÑ Ñ '1', потому что мы должны пропуÑтить нижнее подчеркивание в Ñлучае иÑÐºÐ°Ð¶ÐµÐ½Ð¸Ñ stdcall memcpy(&functionName[1], entryPoint, originalFuncNameLength); memset(&functionName[1] + originalFuncNameLength, 0, kBufferOverhead); // ЕÑли флаг 'dont mangle' не уÑтановлен, Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ 'W' более приоритетна, по Ñравнению Ñ Ð¾Ñ€Ð¸Ð³Ð¸Ð½Ð°Ð»ÑŒÐ½Ñ‹Ð¼ именем чем Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ 'A' (да, Ñерьезно) if (pinvokeArgs.charSet == CHARSET_UNICODE) functionName[originalFuncNameLength] = 'W'; functionPtr = dlsym(dynamicLibrary, functionName.c_str() + 1); if (functionPtr != NULL) return reinterpret_cast<Il2CppMethodPointer>(functionPtr); // ЕÑли поиÑк функции Ñ ÑƒÑ‡ÐµÑ‚Ð¾Ð¼ кодировки Ñимволов неудачен, пробуем Ñ Ð¾Ñ€Ð¸Ð³Ð¸Ð½Ð°Ð»ÑŒÐ½Ñ‹Ð¼ именем functionPtr = dlsym(dynamicLibrary, entryPoint); else functionPtr = dlsym(dynamicLibrary, entryPoint); if (functionPtr != NULL) return reinterpret_cast<Il2CppMethodPointer>(functionPtr); // ЕÑли поиÑк Ñ Ð¾Ñ€Ð¸Ð³Ð¸Ð½Ð°Ð»ÑŒÐ½Ñ‹Ð¼ именем неудачен, пробуем иÑказить Ð¸Ð¼Ñ functionName[originalFuncNameLength] = 'A'; functionPtr = dlsym(dynamicLibrary, functionName.c_str() + 1); return reinterpret_cast<Il2CppMethodPointer>(functionPtr); }
Т.к. isNoMangle
имеет значение
false
, ветвь if (pinvokeArgs.isNoMangle)
не выполнитÑÑ Ð¸ нам нужно Ñделать немного больше работы. Ð’ чаÑтноÑти, мы уÑтанавливаем Ð´Ð»Ñ functionName
Ð¸Ð¼Ñ Ð½Ð°Ñ‚Ð¸Ð²Ð½Ð¾Ð¹ функции, а затем, поÑкольку мы не иÑпользуем CHARSET_UNICODE
, мы игнорируем его и вмеÑто Ñтого вызываем dlsym
Ñ entryPoint
. ЕÑли мы не получим указатель на нативную функцию мы менÑем functionName
и передаем его в dlsym
. Ð’ конце мы приводим тип ÑƒÐºÐ°Ð·Ð°Ñ‚ÐµÐ»Ñ Ð½Ð° нативную функцию к желаемому типу.
Обратите внимание, что ни Ñта Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ð½Ð¸ PlatformInvoke::Resolve
никогда не иÑпользовали тип “ÑÐ¾Ð³Ð»Ð°ÑˆÐµÐ½Ð¸Ñ Ð¾ вызовахâ€. Указание разных типов ÑоглашениÑ, может привеÑти к генерированию разных параметров (например, неиÑпользованный IL2CPP_CALL_DEFAULT
), но, в конце концов, наÑтройки игнорируютÑÑ, еÑли только не выполнÑетÑÑ Ñборка Ð´Ð»Ñ Windows.
Теперь мы можем вернутьÑÑ Ð½Ð°Ð·Ð°Ð´ к нашей extern
функции: PInvoke_sqlite3_exec_m41306BF2F0AF2643E884D0594432D388B61C4974
. ПредполагаÑ, что вÑе работает, перейдем к, непоÑредÑтвенно, Ñамому вызову функции. Предварительно, нам необходимо преобразовать C# string
в Ñтроку в Ñтиле C: char*
, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ ÑвлÑетÑÑ ÑƒÐºÐ°Ð·Ð°Ñ‚ÐµÐ»ÐµÐ¼ на первый Ñимвол маÑÑива Ñимволов. Ðто делает Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ il2cpp_codegen_marshal_string
, из файла /path/to/Unity/installation/Contents/il2cpp/libil2cpp/codegen/il2cpp-codegen-il2cpp.h
:
inline char* il2cpp_codegen_marshal_string(String_t* string) { return il2cpp::vm::PlatformInvoke::MarshalCSharpStringToCppString((RuntimeString*)string); }
Она проÑто передает параметры в il2cpp::vm::PlatformInvoke::MarshalCSharpStringToCppString
, давайте откроем ее в /path/to/Unity/installation/Contents/il2cpp/libil2cpp/vm/PlatformInvoke.cpp
:
char* PlatformInvoke::MarshalCSharpStringToCppString(Il2CppString* managedString) { if (managedString == NULL) return NULL; std::string utf8String = utils::StringUtils::Utf16ToUtf8(managedString->chars); char* nativeString = MarshalAllocateStringBuffer<char>(utf8String.size() + 1); strcpy(nativeString, utf8String.c_str()); return nativeString; }
RuntimeString
Ñто тот же Ñамый тип, что и Il2CppString
, поÑтому параметр не менÑетÑÑ.
Ð’ Ñвою очередь вызываетÑÑ Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ utils::StringUtils::Utf16ToUtf8
чтобы конвертировать 16 битную C# Ñтроку в 8 битную C Ñтроку. Ее можно увидеть в файле /path/to/Unity/installation/Contents/il2cpp/libil2cpp/utils/StringUtils.cpp
:
std::string StringUtils::Utf16ToUtf8(const Il2CppChar* utf16String) { return Utf16ToUtf8(utf16String, -1); } std::string StringUtils::Utf16ToUtf8(const Il2CppChar* utf16String, int maximumSize) { const Il2CppChar* ptr = utf16String; size_t length = 0; while (*ptr) ptr++; length++; if (maximumSize != -1 && length == maximumSize) break; std::string utf8String; utf8String.reserve(length); utf8::unchecked::utf16to8(utf16String, ptr, std::back_inserter(utf8String)); return utf8String; }
ÐŸÐµÑ€Ð²Ð°Ñ Ñ‡Ð°ÑÑ‚ÑŒ получает длину Ñтроки. Ð’Ñ‚Ð¾Ñ€Ð°Ñ Ñ‡Ð°ÑÑ‚ÑŒ резервирует доÑтаточно меÑта в std::string
, затем вызывает utf8::unchecked::utf16to8
чтобы ее заполнить. Ðта Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ð»ÐµÐ¶Ð¸Ñ‚ в файле /path/to/Unity/installation/Contents/il2cpp/libil2cpp/utils/utf8-cpp/source/utf8/unchecked.h
как чаÑÑ‚ÑŒ другой библиотеки. Мы не будем детально раÑÑматривать ее работу Ñ‚.к. Ñто проÑтое преобразование из UTF-16 в UTF-8.
ВозвращаÑÑÑŒ к PlatformInvoke::MarshalCSharpStringToCppString
, мы продолжаем Ð²Ñ‹Ð·Ñ‹Ð²Ð°Ñ MarshalAllocateStringBuffer
. РаÑÑмотрим ее в /path/to/Unity/installation/Contents/il2cpp/libil2cpp/vm/PlatformInvoke.h
:
template<typename T> static T* MarshalAllocateStringBuffer(size_t numberOfCharacters) { return (T*)MarshalAlloc::Allocate(numberOfCharacters * sizeof(T)); }
Таким образом Ñто вÑего лишь вызов MarshalAlloc::Allocate
, давайте проÑледуем по пути до файла в macOS /path/to/Unity/installation/Contents/il2cpp/libil2cpp/os/Posix/MarshalAlloc.cpp
:
void* MarshalAlloc::Allocate(size_t size) { return malloc(size); }
Ðто обертка над базовой функцией C Ð´Ð»Ñ Ð²Ñ‹Ð´ÐµÐ»ÐµÐ½Ð¸Ñ Ð¿Ð°Ð¼Ñти malloc
.
Снова, возвращаÑÑÑŒ к PlatformInvoke::MarshalCSharpStringToCppString
мы видим копирование Ñтроки в Ñвежевыделенную памÑÑ‚ÑŒ и, Ñнова переходим к
PInvoke_sqlite3_exec_m41306BF2F0AF2643E884D0594432D388B61C4974
.
ПоÑле вÑего Ñтого мы, наконец, переходим к вызову нативной функции через указатель, полученный ранее: il2cppPInvokeFunc
. Далее, мы вызываем il2cpp_codegen_marshal_free
. Она раÑполагаетÑÑ Ñ€Ñдом Ñ Ñ„ÑƒÐ½ÐºÑ†Ð¸ÐµÐ¹ Ð²Ñ‹Ð´ÐµÐ»ÐµÐ½Ð¸Ñ Ð¿Ð°Ð¼Ñти /path/to/Unity/installation/Contents/il2cpp/libil2cpp/codegen/il2cpp-codegen-il2cpp.h
:
inline void il2cpp_codegen_marshal_free(void* ptr) { il2cpp::vm::PlatformInvoke::MarshalFree(ptr); }
И Ñнова, параметр передаетÑÑ Ð² другую функцию.Ð’ данном Ñлучае, в MarshalFree
из файла /path/to/Unity/installation/Contents/il2cpp/libil2cpp/vm/PlatformInvoke.cpp
:
void PlatformInvoke::MarshalFree(void* ptr) { if (ptr != NULL) MarshalAlloc::Free(ptr); }
Ожидаемо, MarshalAlloc::Free
находитÑÑ Ð² файле /path/to/Unity/installation/Contents/il2cpp/libil2cpp/os/Posix/MarshalAlloc.cpp
и выглÑдит довольно проÑто:
void MarshalAlloc::Free(void* ptr) { free(ptr); }
По Ñути Ñто обертка нужна Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, чтобы Ñделать вызов нативной функции из C#.
Ð’ конечном итоге выполнÑетÑÑ Ð¾Ñ‡ÐµÐ½ÑŒ много работы, Ð´Ð»Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ¸ библиотеки (например, через dlopen
) и Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¸Ð· нее ÑƒÐºÐ°Ð·Ð°Ñ‚ÐµÐ»Ñ Ð½Ð° функцию (например, через dlsym
), но Ñта работа будет пропущена, Ð±Ð»Ð°Ð³Ð¾Ð´Ð°Ñ€Ñ ÐµÐ´Ð¸Ð½Ñтвенному if
проверÑющему static
переменную при поÑледующих вызовах. Маршалинг Ñтроки, Ñто проÑто процеÑÑ Ð²Ñ‹Ð´ÐµÐ»ÐµÐ½Ð¸Ñ Ð½ÐµÐ±Ð¾Ð»ÑŒÑˆÐ¾Ð³Ð¾ объема памÑти и ÐºÐ¾Ð½Ð²ÐµÑ€Ñ‚Ð°Ñ†Ð¸Ñ Ð¸Ð· 16 битного формата в 8 битный, Ñ Ð¿Ð¾Ñледующим оÑвобождением.
Ð’ целом, вызовы в нативные библиотеки Ñкорее быÑтрые, чем медленные. Они не так быÑтры, как вызовы функций в C#, потому что они получают указатель на функцию, Ð²ÐºÐ»ÑŽÑ‡Ð°Ñ Ð½ÐµÐ¾Ð±Ñ…Ð¾Ð´Ð¸Ð¼Ñ‹Ðµ проверки, и могут включать маршалинг. Ðо нормально делать довольно много таких вызовов за кадр без какой-либо заметной потери производительноÑти.