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

Эффективное взаимодействие между языками.

В Plugify маршалинг во время выполнения — это процесс преобразования типов данных между управляемым и неуправляемым кодом, обеспечивающий бесшовное взаимодействие между плагинами, написанными на разных языках программирования. Это руководство содержит советы и рекомендации для разработчиков языковых модулей о том, как эффективно обрабатывать маршалинг и как использовать утилиты Plugify для упрощения этого процесса.

Базовое сопоставление типов

В следующей таблице перечислены способы представления типов в C++ API:

Тип C++Псевдоним PlugifyПоддержка ссылок ?
voidvoid
boolbool
charchar8
char16_tchar16
int8_tint8
int16_tint16
int32_tint32
int64_tint64
uint8_tuint8
uint16_tuint16
uint32_tuint32
uint64_tuint64
uintptr_tptr64
uintptr_tptr32
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<uintptr_t>ptr64
plg::vector<uintptr_t>ptr32
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

Обработка типов Plugify

Plugify требует два типа маршалинга:

  1. Из языка в C++: Маршалинг данных из языка (например, Go, Python) в типы C++.
  2. Из C++ в язык: Генерация и экспорт функций C++, чтобы их можно было вызывать из других языков.

Маршалинг из языка в C++

Это процесс преобразования данных из нативных типов языка в типы C++ от Plugify (например, plg::string, plg::vector). Обычно это делается, когда плагин вызывает функцию, предоставленную другим плагином.

Пример: Работа с plg::string в C

Вот как можно работать с plg::string из другого языка несколькими способами. Простой подход — использовать нативную систему языка, однако в этом примере мы рассмотрим альтернативный подход для языка, у которого нет такой функции.

На машинном уровне объекты C++ от Plugify по сути являются структурами C. Единственное отличие заключается в том, что они требуют вызова конструкторов и деструкторов. Рассматривая эти объекты как простые структуры C, вы можете избежать генерации оберток во время выполнения и вместо этого использовать маршалинг во время компиляции для взаимодействия с ними.

Код C++ (Языковой модуль)

language-module.cpp
// C++
extern "C" plg::string ConstructString(_GoString_ source) {
    if (source.p == nullptr || source.n == 0)
        return {};
    else
        return { source.p, source.n };
}
extern "C" void DestroyString(plg::string* string) {
    string->~basic_string();
}
extern "C" ptrdiff_t GetStringLength(plg::string* string) {
    return static_cast<ptrdiff_t>(string->length());
}
extern "C" const char* GetStringData(plg::string* string) {
    return string->c_str();
}
extern "C" void AssignString(plg::string* string, _GoString_ source) {
    if (source.p == nullptr || source.n == 0)
        string->clear();
    else
        string->assign(source.p, source.n);
}

Код C (CGo)

plugify.c
typedef struct { char* data; size_t size; size_t cap; } String; // always 24 bytes
typedef struct { void* begin; void* end; void* capacity; } Vector; // always 24 bytes

String Plugify_ConstructString(_GoString_ source) {
    return constructString(source);
}
void Plugify_DestroyString(String* string) {
    destroyString(string);
}
const char* Plugify_GetStringData(String* string) {
    return getStringData(string);
}
ptrdiff_t Plugify_GetStringLength(String* string) {
    return getStringLength(string);
}
void Plugify_AssignString(String* string, _GoString_ source) {
    assignString(string, source);
}

typedef String (*ParamCallbackFn)(int32_t, float, double, Vector4*, int64, char, String*);
static String ParamCallback(int32_t a, float b, double c, Vector4* d, int64 e, char f, String* g) {
    static Param7CallbackFn __func = NULL;
    if (__func == NULL) Plugify_GetMethodPtr2("other_plugin.ParamCallback", (void**)&__func); // get address of original function
    return __func(a, b, c, d, e, f, g);
}

Использование в Go

plugin.go
func ParamCallback(a int32, b float32, c float64, d Vector4, e int64, f int8, g string) string {
    __a := C.int32_t(a)
    __b := C.float(b)
    __c := C.double(c)
    __e := C.int64_t(c)
    __d := *(*C.Vector4)(unsafe.Pointer(&d))
    __f := C.char(f)
    __g := C.Plugify_ConstructString(g) // [!] call plg::string ctor on stack-allocated struct
    
    // Call the C++ function by address 
    // plg::string ParamCallback(int32_t a, float b, double c, const plg::vec4& d, int64_t e, int8_t f, const plg::string& g)
    __r := C.ParamCallback(__a, __b, __c, &__d, &__e, __f, &__g)
 
    // Get pointer to cstring and convert to go equivalent
    output := C.GoStringN(
        C.Plugify_GetStringData(&__r), 
        C.Plugify_GetStringLength(&__r)
    )
 
    // Clean up
    C.Plugify_DestroyString(&__g)
    C.Plugify_DestroyString(&__r)
    
    return output;
}

Преимущества этого подхода

  1. Отсутствие накладных расходов во время выполнения: Избегает необходимости в генерации функций во время выполнения, снижая накладные расходы на производительность.
  2. Совместимость с языками: Работает с языками, которыми нельзя управлять из неуправляемого кода во время выполнения.
  3. Явное управление памятью: Обеспечивает полный контроль над жизненным циклом объектов и выделением памяти.

Важные моменты

  • Убедитесь, что расположение в памяти и выравнивание типов Plugify совпадают со структурами C, используемыми в целевом языке.
  • Управляйте созданием и уничтожением объектов вручную, чтобы избежать утечек памяти или неопределенного поведения.

Экспорт функций из языка в C++

Plugify требует, чтобы плагины экспортировали функции, написанные на их родном языке (например, Python, Go), чтобы их можно было вызывать из других плагинов. Этот процесс включает создание оберток, которые преобразуют типы C++ от Plugify в типы целевого языка и наоборот. Эти обертки генерируются во время загрузки плагина и сохраняются в LoadResult для последующего экспорта в другие языковые модули.

Как это работает

  1. Загрузка плагина: При загрузке плагина языковой модуль инициализирует скрипт плагина и извлекает его экспортированные функции.
  2. Создание обертки: Для каждой экспортированной функции языковой модуль создает обертку с помощью JitCallback или аналогичного механизма. Эта обертка обрабатывает преобразование типов между C++ типами Plugify и типами целевого языка.
  3. Экспорт в другие модули: Обертки сохраняются в LoadResult и экспортируются в другие языковые модули на этапе OnMethodExport.

Пример: Языковой модуль Python

Вот упрощенный пример того, как языковой модуль Python экспортирует функцию (add_numbers) в C++:

Код плагина на Python

plugin.py
def add_numbers(a: int, b: int) -> int:
    """
    Adds two 32-bit integers.
    :param a: First integer
    :param b: Second integer
    :return: Sum of a and b
    """
    a_32 = c_int32(a).value
    b_32 = c_int32(b).value
    result = c_int32(a_32 + b_32).value
    return result

Код C++ (Языковой модуль Python)

Языковой модуль Python создает обертку для функции add_numbers во время загрузки плагина. Эта обертка преобразует типы C++ в типы Python, вызывает функцию Python и преобразует результат обратно в тип C++.

language-module.cpp
// InternalCall: Handles the actual function call and type conversion
void InternalCall(MethodHandle method, MemAddr data, const Parameters* params, uint8_t count, const Return* ret) {
    // Convert C++ types to Python types
    int32_t a = params->GetArgument<int32_t>(0);
    int32_t b = params->GetArgument<int32_t>(1);
    
    // Create arguments for the Python function
    PyObject* pArgs = PyTuple_Pack(2, PyLong_FromLong(a), PyLong_FromLong(b));
    
    // Extract the Python function from data
    PyObject* pFunc = reinterpret_cast<PyObject*>(data);
    
    // Call the Python function
    PyObject* pValue = PyObject_CallObject(pFunc, pArgs);
    int32_t result = -1;  // Default error value
    
    if (pValue) {
        result = PyLong_AsLong(pValue);
        Py_DECREF(pValue);
    } else {
        PyErr_Print();
        std::cerr << "Function call failed" << std::endl;
    }
    
    // Cleanup
    Py_DECREF(pArgs);
    Py_DECREF(pFunc);
    
    // Set the return value
    ret->SetReturn(result);
}

// OnPluginLoad: Creates wrappers for exported functions
LoadResult PythonLanguageModule::OnPluginLoad(PluginHandle plugin) {
    // ...
    
    std::span<const MethodHandle> exportedMethods = plugin.GetDescriptor().GetExportedMethods();
    std::vector<MethodData> methods;
    methods.reserve(exportedMethods.size());

    for (const auto& method : exportedMethods) {
        // Retrieve the Python function by name
        if (PyObject* pFunc = script->GetFunctionByName(method.GetFunctionName())) {
            // Create a JIT wrapper for the function
            JitCallback callback(_rt);
            MemAddr func = callback.GetJitFunc(method, &InternalCall, pFunc);

            if (!func) {
                // Handle errors
                funcErrors.emplace_back(method.GetName());
                continue;
            }

            // Store the wrapper and method data
            _functions.emplace_back(std::move(callback));
            methods.emplace_back(method, func);
        } else {
            // Handle missing functions
            funcErrors.emplace_back(method.GetName());
        }
    }

    // Return errors if any functions were not found
    if (!funcErrors.empty()) {
        std::string funcs(funcErrors[0]);
        for (auto it = std::next(funcErrors.begin()); it != funcErrors.end(); ++it) {
            std::format_to(std::back_inserter(funcs), ", {}", *it);
        }
        return ErrorData{ std::format("Not found {} method function(s)", funcs) };
    }

    // Return the list of wrapped methods
    return LoadResultData{ std::move(methods) };
}

Ключевые шаги в процессе

  1. Извлечение функции: Языковой модуль извлекает экспортированные функции плагина (например, add_numbers в Python).
  2. Создание обертки: Для каждой функции создается обертка с помощью JitCallback. Эта обертка обрабатывает преобразование типов и вызов функции.
  3. Обработка ошибок: Если функция не может быть найдена или обертка не может быть создана, языковой модуль регистрирует ошибку и пропускает функцию.
  4. Экспорт в другие модули: Обернутые функции сохраняются в LoadResult и экспортируются в другие языковые модули на этапе OnMethodExport.

Преимущества этого подхода

  1. Независимость от языка: Функции, написанные на любом языке, могут быть экспортированы и вызваны другими плагинами.
  2. Безопасность типов: Обертки обеспечивают правильное преобразование типов между языками.
  3. Производительность: За счет генерации оберток во время загрузки накладные расходы во время выполнения минимизируются.

Важные моменты

  1. Управление памятью: Обеспечьте правильную очистку ресурсов (например, объектов Python), чтобы избежать утечек памяти.
  2. Обработка ошибок: Корректно обрабатывайте ошибки, особенно когда функции отсутствуют или преобразование типов завершается неудачно.
  3. Производительность: Оптимизируйте обертки для приложений, критичных к производительности.

Использование библиотеки Jit

Библиотека plugify-jit — это мощный инструмент для генерации функций во время выполнения и динамических вызовов функций. Она предоставляет два ключевых класса, JitCallback и JitCall, которые необходимы для маршалинга функций между управляемым и неуправляемым кодом.

JitCallback

Класс JitCallback позволяет создавать объекты обратного вызова, которые можно передавать функциям как указатели на функции обратного вызова. Эти объекты обеспечивают динамическую итерацию по аргументам при вызове колбэка.

Ключевые методы

  • GetJitFunc: Генерирует динамически созданную функцию на основе ссылки на метод.
    MemAddr GetJitFunc(MethodHandle method, CallbackHandler callback, MemAddr data = nullptr, HiddenParam hidden = &ValueUtils::IsHiddenParam);
    
  • CallbackHandler: Тип функции, который обрабатывает логику обратного вызова.
    using CallbackHandler = void(*)(MethodHandle method, MemAddr data, const Parameters* params, uint8_t count, const Return* ret);
    

Пошаговое руководство

Инициализируйте объект:

Создайте экземпляр класса JitCallback. Ему требуется объект времени выполнения asmjit для выделения исполняемой памяти.

JitCallback callback(jitRuntime);
Сгенерируйте функцию:

Используйте метод GetJitFunc для генерации указателя на функцию.

void* methodAddr = callback.GetJitFunc(method, &Callback, funcAddress);
Реализуйте функцию обратного вызова:

Определите функцию обратного вызова для обработки преобразования типов и вызова исходной функции.

void Callback(MethodHandle method, MemAddr data, const Parameters* params, uint8_t count, const Return* ret) {
    // Convert types and call the original function
}

Пример

void Callback(plugify::MethodHandle method, plugify::MemAddr data, const plugify::JitCallback::Parameters* params, uint8_t count, const plugify::JitCallback::Return* ret) {
// Implementation of the callback function
}

int main() {
    // Initialize the JitCallback object
    plugify::JitCallback callback(jitRuntime);

    // Define the method and function pointers
    plugify::MethodHandle method;  // Assume this is properly initialized
    void* func = /* function pointer to be used or any other data */;

    // Generate the JIT function (C Calling Convention)
    void* methodAddr = callback.GetJitFunc(method, &Callback, func);
}

JitCall

Класс JitCall инкапсулирует семантику вызова функций, позволяя динамически передавать параметры функций и выполнять вызовы. Это особенно полезно для динамического вызова функций C.

Ключевые методы

  • GetJitFunc: Генерирует динамически созданную функцию на основе ссылки на метод.
    MemAddr GetJitFunc(MethodHandle method, MemAddr target, WaitType waitType = WaitType::None, HiddenParam hidden = &ValueUtils::IsHiddenParam);
    

Пошаговое руководство

Инициализируйте объект:

Создайте экземпляр класса JitCall.

JitCall call(jitRuntime);
Сгенерируйте функцию:

Используйте метод GetJitFunc для генерации указателя на функцию.

void* methodAddr = call.GetJitFunc(method, funcAddress);

Пример

int main() {
    // Initialize the JitCall object
    plugify::JitCall call(jitRuntime);

    // Define the method and function pointers
    plugify::MethodHandle method;  // Assume this is properly initialized
    void* func = /* function pointer to be used */;

    // Generate the JIT function (C Calling Convention)
    MemAddr methodAddr = call.GetJitFunc(method, func);

    // Generate parameters holder
    plugify::JitCall::Parameters params(2);

    // Pass parameters
    params.AddArgument(2);
    params.AddArgument(2.0f);
    params.AddArgument(2.0);

    // Pass a string by reference
    plg::string str("Some string");
    params.AddArgument(&str);

    // Call the function
    plugify::JitCall::Return ret{};
    methodAddr.Cast<CallingFunc>()(params.GetDataPtr(), &ret);

    // Validate the return value
    assert(ret.GetReturn<int>() == 1);
}

Преимущества использования библиотеки Jit

  1. Динамическая генерация функций: Создавайте и вызывайте функции во время выполнения, обеспечивая гибкость и адаптируемость.
  2. Интероперабельность: Облегчает взаимодействие между различными языками программирования в рамках Plugify.
  3. Упрощенная интеграция: Предоставляет простой API для генерации и использования динамических функций, снижая сложность маршалинга.

Линковка библиотеки

Чтобы слинковать библиотеку plugify-jit с вашим языковым модулем, добавьте plugify::plugify-jit в вашу цель CMake:

target_link_libraries(${PROJECT_NAME} PRIVATE plugify::plugify plugify::plugify-jit)

Это гарантирует, что библиотека plugify-jit будет слинкована во время процесса сборки, делая ее функциональность доступной для вашего кода.

Устранение неполадок

Распространенные проблемы

  1. Утечки памяти:
    • Убедитесь, что динамически выделенная память (например, для объектов JitCallback или JitCall) управляется должным образом.
    • Используйте умные указатели или идиому RAII, чтобы избежать утечек.
  2. Неверные ссылки на методы:
    • Убедитесь, что объект method, переданный в GetJitFunc, действителен и правильно представляет целевой метод.
  3. Советы по отладке:
    • Используйте подробное логирование для отслеживания вызовов функций и значений параметров.
    • Включите отладочные символы в вашей конфигурации сборки, чтобы упростить отладку.

Советы по производительности

  1. Минимизируйте выделение памяти:
    • По возможности повторно используйте объекты JitCallback и JitCall, чтобы избежать частых выделений памяти.
    • Используйте буферы, выделенные на стеке, для небольших структур данных.
  2. Избегайте ненужных преобразований типов:
    • По возможности используйте нативные типы, чтобы уменьшить накладные расходы.
    • Кэшируйте преобразованные значения, если они часто используются повторно.
  3. Оптимизируйте функции обратного вызова:
    • Делайте функции обратного вызова легковесными и избегайте блокирующих операций.
    • Используйте асинхронную обработку для трудоемких задач.