Маршалинг во время выполнения

Как значения передаются между плагинами на разных языках и как языковой модуль пользуется JitCallback и JitCall.

Каждый метод плагина — нативная функция с соглашением о вызовах C данной платформы. Маршалинг — то, что делает языковой модуль, чтобы превратить значения своего языка в аргументы такой функции и обратно. На этой странице перечислены типы, правила передачи и генераторы кода, которые ядро даёт модулям.

Типы

Тип в манифестеТип C++Как передаётся
bool, char8, char16, целые, float, double, ptr64bool, char, char16_t, int8_t … uint64_t, float, double, void*Значением
functionУказатель на нативную функциюЗначением. Его сигнатура — prototype параметра
stringplg::stringУказателем
anyplg::anyУказателем
T[]plg::vector<T>Указателем
vec2, vec3, vec4, mat4x4plg::vec2, plg::vec3, plg::vec4, plg::mat4x4Указателем
Любой тип с "ref": trueT&Указателем: вызываемая функция может изменить значение, и вызывающий увидит изменение

На 32-битных платформах тип указателя называется ptr32. Параметр с enum передаётся как его целый тип.

Полный список типов с именами в C++:

Тип C++Тип PlugifyПоддержка ref
voidvoid❌
boolbool✅
charchar8✅
char16_tchar16✅
int8_tint8✅
int16_tint16✅
int32_tint32✅
int64_tint64✅
uint8_tuint8✅
uint16_tuint16✅
uint32_tuint32✅
uint64_tuint64✅
void*ptr64✅
floatfloat✅
doubledouble✅
void*function❌
plg::stringstring✅
plg::anyany✅
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::vec2vec2✅
plg::vec3vec3✅
plg::vec4vec4✅
plg::mat4x4mat4x4✅

Возвращаемые значения

Небольшие значения возвращаются в регистрах. Более крупные записываются в память, которую выделяет вызывающий; она передаётся скрытым первым аргументом:

  • всегда для 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

plugify/callback.hpp
using CallbackHandler = void (*)(
    const Method* method,  // the signature
    Address data,          // the value passed to GetJitFunc
    uint64_t* params,      // one 64-bit slot per argument
    size_t count,          // number of slots
    void* ret              // where to write the return value
);

Address GetJitFunc(const Method& method, CallbackHandler callback, Address data = nullptr,
                   HiddenParam hidden = &ValueUtils::IsHiddenParam);

Каждый аргумент занимает один слот. Скаляры хранятся в самом слоте; строки, массивы, векторы и ref-параметры хранятся как указатели. ParametersSpan и ReturnSlot читают и пишут слоты:

// The handler behind a method `int32 Add(int32 a, int32 b)`
static void InternalCall(const Method* method, Address data, uint64_t* p, size_t count, void* r) {
    ParametersSpan params(p, count);
    ReturnSlot ret(r, sizeof(int32_t));
    int32_t a = params.Get<int32_t>(0);
    int32_t b = params.Get<int32_t>(1);
    ret.Set<int32_t>(a + b);
}

JitCallback callback;
Address func = callback.GetJitFunc(method, &InternalCall, userData);
if (!func) {
    // callback.GetError() says why
}

data обычно указывает на объект вашего языка, реализующий метод, например функцию Python, так что один обработчик обслуживает все методы. Если метод возвращает объект через скрытый аргумент, p[0] — память, в которой нужно сконструировать результат; для этого есть ReturnSlot::Construct<T>().

JitCall

plugify/call.hpp
using CallingFunc = void (*)(const uint64_t* params, const Return* ret);

Address GetJitFunc(const Method& method, Address target, WaitType waitType = WaitType::None,
                   HiddenParam hidden = &ValueUtils::IsHiddenParam);

Заполните Parameters, по одному значению на аргумент, вызовите сгенерированную функцию и прочитайте результат из Return:

// Call `int32 Add(int32 a, int32 b)` of another plugin
JitCall call;
Address func = call.GetJitFunc(method, target);

Parameters params(2);
params.Add(int32_t{ 2 }).Add(int32_t{ 3 });

Return ret;
func.As<JitCall::CallingFunc>()(params.Get(), &ret);
int32_t sum = ret.Get<int32_t>();  // 5

Для аргумента 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 тестами, см. Создание языкового модуля.