Runtime Marshalling
How values are passed between plugins in different languages, and how a language module uses JitCallback and JitCall.
Every method of a plugin is a native function with the C calling convention of the platform. Marshalling is what a language module does to turn the values of its language into the arguments of such a function and back. This page lists the types, the passing rules, and the code generators the core provides for modules.
Types
| Manifest type | C++ type | Passed as |
|---|---|---|
bool, char8, char16, integers, float, double, ptr64 | bool, char, char16_t, int8_t … uint64_t, float, double, void* | Value |
function | Pointer to a native function | Value. Its signature is the prototype of the parameter |
string | plg::string | Pointer |
any | plg::any | Pointer |
T[] | plg::vector<T> | Pointer |
vec2, vec3, vec4, mat4x4 | plg::vec2, plg::vec3, plg::vec4, plg::mat4x4 | Pointer |
Any type with "ref": true | T& | Pointer: the callee can change the value and the caller sees the change |
On 32-bit platforms the pointer type is called ptr32. An enum parameter is passed as its integer type.
The full list of types with their C++ names:
| 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 | ✅ |
| 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 | ✅ |
Return values
Small values come back in registers. Larger ones are written to memory the caller provides, which is passed as a hidden first argument:
- always for
string,any, every array andmat4x4; - for
vec3andvec4on Windows; on Linux and macOS (x86-64 System V and ARM64) they come back in registers.
vec2 and all scalars come back in registers everywhere. JitCallback and JitCall handle this by themselves; it matters only when you write the native code by hand.
The plg containers
plg::string, plg::vector, plg::any and the vector types come from the headers of the core (include/plg/). They have the same memory layout whatever compiler or standard library builds the module, which is why they, and not std::string or std::vector, cross the boundary between plugins. A module can create and destroy them in its own C++ code and give the language only pointers and plain data. Languages that cannot run C++ constructors describe the same layout in their own terms, for example the plugin packages for C#, Go and Rust.
Generating functions at run time
The core includes two code generators built on asmjit. They are part of the plugify library, nothing else has to be linked. Both work from a Method, the description of a signature in the manifest, so they follow the calling convention, the hidden return argument and ref parameters of that method.
| Class | Generates | Used for |
|---|---|---|
JitCallback | A native function that collects its arguments and calls your handler | Exporting methods of your plugins, and passing functions of your language as function parameters |
JitCall | A function that takes an array of arguments and calls a native function with them | Calling methods of other plugins and function values your plugins receive |
Each generated function lives as long as the JitCallback or JitCall object that made it, so keep these objects for as long as the function can be called.
JitCallback
Each argument takes one slot. Scalars are stored in the slot; strings, arrays, vectors and ref parameters are stored as pointers. ParametersSpan and ReturnSlot read and write the slots:
data usually points to the object of your language that implements the method, for example a Python function, so one handler can serve every method. When the method returns an object through the hidden argument, p[0] is the memory to construct the result in; ReturnSlot::Construct<T>() helps with that.
JitCall
Fill Parameters with one value per argument, call the generated function and read the result from Return:
For a string argument add a pointer to a plg::string; for a method that returns a string, add a pointer to the memory for the result as the first argument and construct nothing there yourself: the callee does it. WaitType::Breakpoint and WaitType::Wait_Keypress stop before the call, which helps when debugging a module.
Calling conventions and variadic methods
By default methods use the C calling convention of the platform. callConv in a method of the manifest sets another one (cdecl, stdcall, fastcall, vectorcall, thiscall, x64systemv, x64windows and others) for code that needs it. varIndex is the index of the first variadic parameter. Both are taken into account by JitCall and JitCallback; native languages call such functions directly.
Tips
- Calls between two plugins of the same language can skip marshalling: the module knows both sides and can call the plugin code directly, as the C# module does for C# plugins.
- Create the
JitCallbackandJitCallobjects once, when a plugin is loaded or exported, not on every call. - A
functionvalue received from another plugin is just an address. Generate aJitCallfor it with theprototypeof the parameter before calling it from your language. - Test the module with the cross-call tests, see Writing Language Module.