Runtime Marshalling
Interfacing between languages efficiently.
In Plugify, runtime marshalling is the process of converting data types between managed and unmanaged code, enabling seamless communication between plugins written in different programming languages. This guide provides tips and recommendations for language module developers on how to handle marshalling efficiently and how to use Plugify's utilities to simplify the process.
Basic Type Mapping
The following table lists how types are exposed to the C++ API:
C++ Type | Plugify Alias | Ref Support ? |
---|---|---|
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 | ✅ |
uintptr_t | ptr64 | ✅ |
uintptr_t | ptr32 | ✅ |
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<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::vec2 | vec2 | ✅ |
plg::vec3 | vec3 | ✅ |
plg::vec4 | vec4 | ✅ |
plg::mat4x4 | mat4x4 | ✅ |
Handling Plugify Types
Plugify requires two types of marshalling:
- Language to C++: Marshalling data from a language (e.g., Go, Python) into C++ types.
- C++ to Language: Generate and Export C++ functions so they can be called from other languages.
Marshalling from Language to C++
This is the process of converting data from a language's native types into Plugify's C++ types (e.g., plg::string
, plg::vector
). This is typically done when a plugin calls a function exposed by another plugin.
Example: Working with plg::string
in C
Here’s how you can work with plg::string
from another language in multiple ways. Simple approach is to use language's native system, however in this example we review alternative approach for language which not have that feature.
At the machine level, Plugify's C++ objects are essentially C structures. The only difference is that they required execution of constructors and destructors. By treating these objects as plain C structures, you can avoid runtime wrapper generation and instead use compile-time marshalling to interact with them.
C++ Code (Language Module)
C Code (CGo)
Go Usage
Advantages of This Approach
- No Runtime Overhead: Avoids the need for runtime function generation, reducing performance overhead.
- Language Compatibility: Works with languages that cannot be controlled from unmanaged code at runtime.
- Explicit Memory Management: Provides full control over object lifetime and memory allocation.
Considerations
- Ensure that the memory layout and alignment of Plugify types match the C structures used in the target language.
- Manually manage object construction and destruction to avoid memory leaks or undefined behavior.
Exporting Functions from Language to C++
Plugify requires plugins to export functions written in their native language (e.g., Python, Go) so they can be called by other plugins. This process involves creating wrappers that convert Plugify's C++ types into the target language's types and vice versa. These wrappers are generated during plugin load and stored in the LoadResult
for later export to other language modules.
How It Works
- Plugin Load: When a plugin is loaded, the language module initializes the plugin's script and retrieves its exported functions.
- Wrapper Creation: For each exported function, the language module creates a wrapper using
JitCallback
or a similar mechanism. This wrapper handles type conversion between Plugify's C++ types and the target language's types. - Export to Other Modules: The wrappers are stored in the
LoadResult
and exported to other language modules during theOnMethodExport
phase.
Example: Python Language Module
Here’s a simplified example of how the Python language module exports a function (add_numbers
) to C++:
Python Plugin Code
C++ Code (Python Language Module)
The Python language module creates a wrapper for the add_numbers
function during plugin load. This wrapper converts C++ types to Python types, calls the Python function, and converts the result back to a C++ type.
For plugins written in the same language, good approach is bypass marshalling entirely. This optimization is particularly useful for performance-critical applications.
Key Steps in the Process
- Function Retrieval: The language module retrieves the plugin's exported functions (e.g.,
add_numbers
in Python). - Wrapper Creation: For each function, a wrapper is created using
JitCallback
. This wrapper handles type conversion and function invocation. - Error Handling: If a function cannot be found or a wrapper cannot be created, the language module logs an error and skips the function.
- Export to Other Modules: The wrapped functions are stored in the
LoadResult
and exported to other language modules during theOnMethodExport
phase.
Advantages of This Approach
- Language Independence: Functions written in any language can be exported and called by other plugins.
- Type Safety: Wrappers ensure that types are correctly converted between languages.
- Performance: By generating wrappers at load time, runtime overhead is minimized.
Considerations
- Memory Management: Ensure proper cleanup of resources (e.g., Python objects) to avoid memory leaks.
- Error Handling: Handle errors gracefully, especially when functions are missing or type conversion fails.
- Performance: Optimize wrappers for performance-critical applications.
Using the Jit Library
The plugify-jit
library is a powerful tool for runtime function generation and dynamic function calls. It provides two key classes, JitCallback
and JitCall
, which are essential for marshalling functions between managed and unmanaged code.
JitCallback
The JitCallback
class allows you to create callback objects that can be passed to functions as callback function pointers. These objects enable dynamic iteration over arguments when the callback is invoked.
Key Methods
GetJitFunc
: Generates a dynamically created function based on a method reference.CallbackHandler
: A function type that handles the callback logic.
Step-by-Step Guide
Initialize the Object:
Create an instance of the JitCallback
class. It requires an asmjit
runtime object for executable memory allocation.
The generated function will be deallocated when the JitCallback
object goes out of scope. Ensure the object remains in scope as long as the function is needed, or use smart pointers to manage its lifetime.
Generate the Function:
Use the GetJitFunc
method to generate a function pointer.
Ensure the method
object is valid and correctly represents the method for which you want to generate a callback function.
Implement the Callback Function:
Define a callback function to handle type conversion and call the original function.
Example
JitCall
The JitCall
class encapsulates function call semantics, allowing you to dynamically push function parameters and issue calls. This is particularly useful for calling C functions in a dynamic manner.
Key Methods
GetJitFunc
: Generates a dynamically created function based on a method reference.
Step-by-Step Guide
Initialize the Object:
Create an instance of the JitCall
class.
The generated function will be deallocated when the JitCall
object goes out of scope. Ensure the object remains in scope as long as the function is needed.
Generate the Function:
Use the GetJitFunc
method to generate a function pointer.
Example
Benefits of Using Jit Library
- Dynamic Function Generation: Create and call functions at runtime, enabling flexibility and adaptability.
- Interoperability: Facilitates communication between different programming languages within Plugify.
- Simplified Integration: Provides a straightforward API for generating and using dynamic functions, reducing the complexity of marshalling.
Linking the Library
To link the plugify-jit
library with your language module, add plugify::plugify-jit
to your CMake target:
This ensures that the plugify-jit
library is linked during the build process, making its functionality available to your code.
Troubleshooting
Common Issues
- Memory Leaks:
- Ensure that dynamically allocated memory (e.g., for
JitCallback
orJitCall
objects) is properly managed. - Use smart pointers or RAII patterns to avoid leaks.
- Ensure that dynamically allocated memory (e.g., for
- Invalid Method References:
- Verify that the
method
object passed toGetJitFunc
is valid and correctly represents the target method.
- Verify that the
- Debugging Tips:
- Use verbose logging to trace function calls and parameter values.
- Enable debugging symbols in your build configuration to simplify debugging.
Performance Tips
- Minimize Memory Allocations:
- Reuse
JitCallback
andJitCall
objects where possible to avoid frequent memory allocations. - Use stack-allocated buffers for small data structures.
- Reuse
- Avoid Unnecessary Type Conversions:
- Use native types whenever possible to reduce overhead.
- Cache converted values if they are reused frequently.
- Optimize Callback Functions:
- Keep callback functions lightweight and avoid blocking operations.
- Use asynchronous processing for time-consuming tasks.