Маршалинг во время выполнения
Как значения передаются между плагинами на разных языках и как языковой модуль пользуется JitCallback и JitCall.
Каждый метод плагина — нативная функция с соглашением о вызовах C данной платформы. Маршалинг — то, что делает языковой модуль, чтобы превратить значения своего языка в аргументы такой функции и обратно. На этой странице перечислены типы, правила передачи и генераторы кода, которые ядро даёт модулям.
Типы
| Тип в манифесте | Тип C++ | Как передаётся |
|---|---|---|
bool, char8, char16, целые, float, double, ptr64 | bool, char, char16_t, int8_t … uint64_t, float, double, void* | Значением |
function | Указатель на нативную функцию | Значением. Его сигнатура — prototype параметра |
string | plg::string | Указателем |
any | plg::any | Указателем |
T[] | plg::vector<T> | Указателем |
vec2, vec3, vec4, mat4x4 | plg::vec2, plg::vec3, plg::vec4, plg::mat4x4 | Указателем |
Любой тип с "ref": true | T& | Указателем: вызываемая функция может изменить значение, и вызывающий увидит изменение |
На 32-битных платформах тип указателя называется ptr32. Параметр с enum передаётся как его целый тип.
Полный список типов с именами в C++:
| Тип C++ | Тип Plugify | Поддержка ref |
|---|---|---|
| void | void | ❌ |
| bool | bool | ✅ |
| char | char8 | ✅ |
| char16_t | char16 | ✅ |
| int8_t | int8 | ✅ |
| int16_t | int16 | ✅ |
| int32_t | int32 | ✅ |
| int64_t | int64 | ✅ |
| uint8_t | uint8 | ✅ |
| uint16_t | uint16 | ✅ |
| uint32_t | uint32 | ✅ |
| uint64_t | uint64 | ✅ |
| void* | ptr64 | ✅ |
| float | float | ✅ |
| double | double | ✅ |
| void* | function | ❌ |
| plg::string | string | ✅ |
| plg::any | any | ✅ |
| plg::vector<bool> | bool | ✅ |
| plg::vector<char> | char8 | ✅ |
| plg::vector<char16_t> | char16 | ✅ |
| plg::vector<int8_t> | int8 | ✅ |
| plg::vector<int16_t> | int16 | ✅ |
| plg::vector<int32_t> | int32 | ✅ |
| plg::vector<int64_t> | int64 | ✅ |
| plg::vector<uint8_t> | uint8 | ✅ |
| plg::vector<uint16_t> | uint16 | ✅ |
| plg::vector<uint32_t> | uint32 | ✅ |
| plg::vector<uint64_t> | uint64 | ✅ |
| plg::vector<void*> | ptr64 | ✅ |
| plg::vector<float> | float | ✅ |
| plg::vector<double> | double | ✅ |
| plg::vector<plg::string> | string | ✅ |
| plg::vector<plg::any> | any | ✅ |
| plg::vector<plg::vec2> | vec2 | ✅ |
| plg::vector<plg::vec3> | vec3 | ✅ |
| plg::vector<plg::vec4> | vec4 | ✅ |
| plg::vector<plg::mat4x4> | mat4x4 | ✅ |
| plg::vec2 | vec2 | ✅ |
| plg::vec3 | vec3 | ✅ |
| plg::vec4 | vec4 | ✅ |
| plg::mat4x4 | mat4x4 | ✅ |
Возвращаемые значения
Небольшие значения возвращаются в регистрах. Более крупные записываются в память, которую выделяет вызывающий; она передаётся скрытым первым аргументом:
- всегда для
string,any, всех массивов иmat4x4; - для
vec3иvec4на Windows; на Linux и macOS (x86-64 System V и ARM64) они возвращаются в регистрах.
vec2 и все скаляры везде возвращаются в регистрах. JitCallback и JitCall учитывают это сами; это важно, только если нативный код пишется вручную.
Контейнеры plg
plg::string, plg::vector, plg::any и векторные типы взяты из заголовков ядра (include/plg/). Их устройство в памяти одинаково, каким бы компилятором и стандартной библиотекой ни был собран модуль, поэтому через границу между плагинами передаются они, а не std::string или std::vector. Модуль может создавать и уничтожать их в своём C++-коде, а языку отдавать только указатели и простые данные. Языки, которые не умеют вызывать конструкторы C++, описывают то же устройство своими средствами, например пакеты плагинов для C#, Go и Rust.
Генерация функций во время выполнения
В ядре есть два генератора кода на asmjit. Они входят в библиотеку plugify, линковать ничего дополнительно не нужно. Оба работают по Method, описанию сигнатуры из манифеста, поэтому учитывают соглашение о вызовах, скрытый аргумент для возврата и ref-параметры метода.
| Класс | Что генерирует | Для чего |
|---|---|---|
JitCallback | Нативную функцию, которая собирает свои аргументы и вызывает ваш обработчик | Экспорт методов ваших плагинов и передача функций вашего языка как параметров function |
JitCall | Функцию, которая принимает массив аргументов и вызывает с ними нативную функцию | Вызов методов других плагинов и значений function, которые получают ваши плагины |
Сгенерированная функция живёт столько же, сколько создавший её объект JitCallback или JitCall, поэтому храните эти объекты, пока функцию могут вызвать.
JitCallback
Каждый аргумент занимает один слот. Скаляры хранятся в самом слоте; строки, массивы, векторы и ref-параметры хранятся как указатели. ParametersSpan и ReturnSlot читают и пишут слоты:
data обычно указывает на объект вашего языка, реализующий метод, например функцию Python, так что один обработчик обслуживает все методы. Если метод возвращает объект через скрытый аргумент, p[0] — память, в которой нужно сконструировать результат; для этого есть ReturnSlot::Construct<T>().
JitCall
Заполните Parameters, по одному значению на аргумент, вызовите сгенерированную функцию и прочитайте результат из Return:
Для аргумента string добавьте указатель на plg::string; для метода, который возвращает string, первым аргументом добавьте указатель на память для результата и ничего в ней не конструируйте: это сделает вызываемая функция. WaitType::Breakpoint и WaitType::Wait_Keypress останавливаются перед вызовом, это помогает при отладке модуля.
Соглашения о вызовах и методы с переменным числом аргументов
По умолчанию методы используют соглашение о вызовах C данной платформы. Поле callConv метода в манифесте задаёт другое (cdecl, stdcall, fastcall, vectorcall, thiscall, x64systemv, x64windows и другие) для кода, которому оно нужно. varIndex — индекс первого параметра переменной части. Оба учитываются JitCall и JitCallback; нативные языки вызывают такие функции напрямую.
Советы
- Вызовы между двумя плагинами одного языка могут обходиться без маршалинга: модуль знает обе стороны и может вызвать код плагина напрямую, как это делает C#-модуль для C#-плагинов.
- Создавайте объекты
JitCallbackиJitCallодин раз, при загрузке или экспорте плагина, а не при каждом вызове. - Значение
function, полученное от другого плагина, — просто адрес. Чтобы вызвать его из своего языка, сгенерируйте для негоJitCallпоprototypeпараметра. - Проверяйте модуль cross-call тестами, см. Создание языкового модуля.