How to Wrap a Real Native Library
There are many posts that’ll tell you the “hello world” of calling native code like C++ from a C# Unity project. These tend to be simple examples though, so it’s hard to go from that to wrapping a real life useful native library. Today we’ll wrap SQLite, a popular C library that implements a database, and talk about the challenges in doing so and how to end up with a pleasant C# API for the rest of the game to use. Read on to learn how!
Getting the Library
First thing’s first, we need to get the library to wrap. Since we’re wrapping SQLite today, we’ll head over to their download page and acquire their recommended amalgamation ZIP file. This contains the whole SQLite library in just three huge source files.
Next, we’ll build the library. Since SQLite’s amalgamation contains the entire library in a single source file, we can simply run one command to build a shared library. Here’s the one command to build it on macOS:
clang -shared -undefined dynamic_lookup -o sqlite3.so sqlite3.c
Now we can drop the shared library (sqlite3.so
) into our Unity project and click on it in the editor’s Project
pane to see its properties in the Inspector
pane. Uncheck Any Platform
and check Editor
and Standalone
instead. The change the CPU to x86_64
and OS to OSX
in the Editor Settings
tab. In the PC, Mac & Linux Standalone Settings
tab, uncheck the Linux
checkboxes and check the Mac OS X
one. Finally, click the Apply
button.
Now we’re done with the GUI and can focus on the code.
P/Invoke Setup
Create a new C# file for the wrapper: Sqlite.cs
. Inside, put a static Sqlite
class with a nested static PInvoke
class like so:
public static class Sqlite { private static class PInvoke { #if UNITY_IOS const string PLUGIN_NAME = "__Internal"; #else const string PLUGIN_NAME = "sqlite3"; #endif } }
This forms the basis of the API. Users of this interface will call functions on Sqlite
and those functions will in turn call functions in PInvoke
to communicate with the native library. Adding the extra level of indirection allows for Sqlite
to massage the native API into a more pleasant, idiomatic C# style as we’ll see shortly.
Next, let’s wrap our first native function: sqlite3_open
. This opens a database file or in-RAM database so that SQL queries can be run on it. Here’s the function’s signature in C:
int sqlite3_open(const char* filename, sqlite3** ppDb);
We can allow C# to call this function by putting a method with the same name in the PInvoke
class:
[DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern ReturnCode sqlite3_open(string filename, out IntPtr ppDb);
Notice that we’ve changed all three types here. First, filename
is a C-string which P/Invoke knows how to map to directly from a string
so it’s safe to change the type to that.
Second, ppDb
is a pointer to a pointer to a sqlite3
. We can replace the first level of pointer with IntPtr
since it is by definition the size of a native pointer. Then we can get the second level of pointer as well as the semantics that it’ll be assigned by the function simply by adding the out
keyword.
Third, we’ve changed the return type from int
to ReturnCode
. This is an enum whose underlying type is int
and adds a little meaning to the return value. Here’s how we define it in the Sqlite
class:
public enum ReturnCode : int { OK = 0, ROW = 100, DONE = 101, }
SQLite has many more return codes, but these will suffice for today.
Next we add the indirection layer in SQLite
to call the sqlite3_open
in PInvoke
:
public static ReturnCode Open(string filename, out Pointer<Database> db) { IntPtr pDb; ReturnCode rc = PInvoke.sqlite3_open(filename, out pDb); db = new Pointer<Database>(pDb); return rc; }
This function adds a few tricks on top of just calling sqlite3_open
. First, it uses a Database
type to stand in for the native sqlite3
type. This is a more idiomatic C# way of naming structures, so usage of the interface will be a little more familiar to C# games. Since we’re just dealing with pointers to one, we don’t really care how it’s defined so let’s just leave it empty:
public struct Database { }
Next, we use a Pointer<Database>
to make a strongly-typed pointer to a Database
. We could have just used an IntPtr
but that would allow for errors to occur when accidentally passing a pointer to something else. Strong typing costs us nothing here as Pointer
is just a struct containing an IntPtr
:
public struct Pointer<T> { public IntPtr Ptr; public Pointer(IntPtr ptr) { Ptr = ptr; } }
To put it all together, we now have a file that looks like this:
using System; using System.Runtime.InteropServices; public static class Sqlite { public struct Database { } public struct Pointer<T> { public IntPtr Ptr; public Pointer(IntPtr ptr) { Ptr = ptr; } } public enum ReturnCode : int { OK = 0, ROW = 100, DONE = 101, } public static ReturnCode Open(string filename, out Pointer<Database> db) { IntPtr pDb; ReturnCode rc = PInvoke.sqlite3_open(filename, out pDb); db = new Pointer<Database>(pDb); return rc; } private static class PInvoke { #if UNITY_IOS const string PLUGIN_NAME = "__Internal"; #else const string PLUGIN_NAME = "sqlite3"; #endif [DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern ReturnCode sqlite3_open( string filename, out IntPtr ppDb); } }
The rest of the game can use it like this:
Sqlite.Pointer<Sqlite.Database> db; Sqlite.ReturnCode rc = Sqlite.Open("/path/to/highscores.db", out db); if (rc != Sqlite.ReturnCode.OK) { print("Error opening DB: " + rc); return; } // ... use DB
More Methods
The rest of the wrapper will follow the pattern of sqlite3_open
. We’ll walk through seven more functions to see how the wrapper takes shape. First up is sqlite3_close
that allows us to close the database we just opened. Here’s how the C signature looks:
int sqlite3_close(sqlite3* pDb);
Here’s how our PInvoke
wrapper looks:
[DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern ReturnCode sqlite3_close(IntPtr pDb);
We use the same trick of replacing the int
return value with a ReturnCode
to get strong typing and enumerated values. Since the parameter is just a pointer, we omit the out
and just take an IntPtr. Now let's look at the
Sqlite` indirection method:
public static ReturnCode Close(Pointer<Database> db) { return PInvoke.sqlite3_close(db.Ptr); }
This is really just a trivial pass-through of the pointer contained in the Pointer<Database>
, so let’s move on to a more interesting function: sqlite3_prepare_v2
. This prepares a SQL statement for execution. It’s C signature looks like this:
int sqlite3_prepare_v2( sqlite3* db, const char* zSql, int nByte, sqlite3_stmt** ppStmt, const char** pzTail);
Like sqlite3_close
, this takes a pointer to the database. It then takes a SQL statement string and number of bytes long that string is. Then it takes an output parameter like sqlite3_open
did, but this time with a new opaque type: sqlite3_stmt
. Finally, it takes another output parameter for the last character read of the given SQL statement string.
Here’s the C# wrapper that goes in PInvoke
:
[DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern ReturnCode sqlite3_prepare_v2( IntPtr pDb, string sql, int sqlLength, out IntPtr ppStmt, IntPtr pzTail);
Once again we’ve placed the int
return value with ReturnCode
, all the pointers with IntPtr
, and the string with string
. The interesting bit is next up in the Sqlite
indirection layer:
public static ReturnCode Prepare( Pointer<Database> db, string sql, out Pointer<Statement> statement) { IntPtr ptr; ReturnCode rc = PInvoke.sqlite3_prepare_v2( db.Ptr, sql, -1, out ptr, IntPtr.Zero); statement = new Pointer<Statement>(ptr); return rc; }
Here we take a strongly-typed Pointer<Statement>
where Statement
is another empty struct:
public struct Statement { }
However, notice that the parameters to Prepare
don’t quite match the parameters to sqlite3_prepare_v2
. We can take fewer parameters because we’re going to pass constants for some of them anyhow. In this case, we pass -1
for the length of the SQL string to indicate that the whole thing should be read. Then we pass IntPtr.Zero
, equivalent to null
, for the last parameter to indicate that we’re not interested in receiving a pointer to the last character of the SQL string. The result is a cleaner interface for the game that includes just what’s really needed. The indirection layer makes this possible.
The next two functions are extremely straightforward: sqlite3_step
and sqlite3_finalize
. Calling sqlite3_step
steps through the results of a SQL statement created by sqlite3_prepare_v2
and sqlite3_finalize
finalizes that statement. Here are there C signatures:
int sqlite3_step(sqlite3_stmt* pStmt); int sqlite3_finalize(sqlite3_stmt* pStmt);
Here’s how we wrap them in PInvoke
:
[DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern ReturnCode sqlite3_step(IntPtr stmt); [DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern ReturnCode sqlite3_finalize(IntPtr stmt);
And here’s the indirection layer:
public static ReturnCode Step(Pointer<Statement> statement) { return PInvoke.sqlite3_step(statement.Ptr); } public static ReturnCode Finalize(Pointer<Statement> statement) { return PInvoke.sqlite3_finalize(statement.Ptr); }
Now for a more interesting function: sqlite3_exec
. This function performs all three of these steps in one shot: sqlite3_prepare_v2
then sqlite3_step
then sqlite3_finalize
. It’s a convenient function to reduce verbosity, especially when there are no results to step through such as a DROP
statement. Here’s how the C signature looks:
int sqlite3_exec( sqlite3* pDb, const char* sql, int (*callback)(void*, int, char**, char**), void* userdata, char** errmsg);
Like sqlite3_prepare_v2
, this function takes the database and SQL query string. It doesn’t take a length of that string nor an optional out parameter for the parsed end of it. Instead, it takes a pointer to a callback function to execute for each row in the results and a userdata
pointer to pass to that callback function. It also takes an out parameter for any error message. Here’s how we’ll wrap it:
[DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern ReturnCode sqlite3_exec( IntPtr pDb, string sql, IntPtr callback, IntPtr userdata, IntPtr errmsg);
The wrapping is very straightforward as we simply replace the SQL string with string
, all the pointers with IntPtr
, and the return int
with ReturnCode
. The interesting part comes in the indirection layer in Sqlite
:
public static ReturnCode Execute(Pointer<Database> database, string sql) { return PInvoke.sqlite3_exec( database.Ptr, sql, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); }
Like with sqlite3_prepare_v2
, we can cut down on the number of parameters the game needs to pass. We’re only going to use this function for SQL statements with no result rows, so we’ll pass IntPtr.Zero
for the callback and userdata
parameters. We’ll also pass IntPtr.Zero
for the error message output since we want to ignore it anyhow. The result is another very simple function for the game to call.
To finish up the wrapper, let’s look at two functions to get columns of a row: sqlite3_column_text
and sqlite3_column_int
. These get text and 32-bit integer columns, respectively. Here are their C signatures:
const unsigned char* sqlite3_column_text(sqlite3_stmt* pStmt, int iCol); int sqlite3_column_int(sqlite3_stmt* pStmt, int iCol);
Here’s how we wrap them:
[DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr sqlite3_column_text(IntPtr stmt, int iCol); [DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern int sqlite3_column_int(IntPtr stmt, int iCol);
Notice that even though sqlite3_column_text
returns a string, we don’t use the string
return type. This is because we need to perform an additional step in the indirection layer:
public static string GetTextColumn( Pointer<Statement> statement, int columnIndex) { IntPtr ptr = PInvoke.sqlite3_column_text(statement.Ptr, columnIndex); return Marshal.PtrToStringAuto(ptr); } public static int GetIntColumn( Pointer<Statement> statement, int columnIndex) { return PInvoke.sqlite3_column_int(statement.Ptr, columnIndex); }
The string returned by sqlite3_column_text
needs to be copied into a managed string
. By default, the P/Invoke marshaller won’t make this copy. So we need to capture the string as just a pointer and then call Marshal.PtrToStringAuto
to force it to make a copy of the C string into a managed string
. The indirection layer gives us the flexibility to make this copy within the interface code so users of it don’t need to worry about the memory management details of the SQLite API.
We’ve now completed our wrapper for today. There’s a lot more of the SQLite API that we could wrap, but we’ve covered a representative portion of it for the purposes of this article. Notice that we didn’t use any “unsafe” code at all in the wrapper, so this is usable without enabling that compiler option or adding any unsafe
keywords.
Here is the final Sqlite.cs
file. Feel free to use it in your projects as-is, as the basis of a more complete wrapper, or as a wrapper for some other native library.
using System; using System.Runtime.InteropServices; /// <summary> /// An interface to SQLite /// </summary> /// /// <author> /// Jackson Dunstan, https://JacksonDunstan.com/articles/5117 /// </author> /// /// <license> /// MIT /// </license> public static class Sqlite { /// <summary> /// An SQLite database /// </summary> public struct Database { } /// <summary> /// An SQLite SQL statement /// </summary> public struct Statement { } /// <summary> /// A pointer to an SQLite type /// </summary> public struct Pointer<T> { /// <summary> /// Pointer to the native memory for the type /// </summary> public IntPtr Ptr; /// <summary> /// Create the pointer /// </summary> /// /// <param name="ptr"> /// Pointer to the native memory for the type /// </param> public Pointer(IntPtr ptr) { Ptr = ptr; } } public enum ReturnCode : int { /// <summary> /// Everything went OK /// </summary> OK = 0, /// <summary> /// A row is available /// </summary> ROW = 100, /// <summary> /// An operation is done /// </summary> DONE = 101, } /// <summary> /// Filename to pass to <see cref="Open"/> to use RAM instead of a file /// </summary> public const string MemoryFilename = ":memory:"; /// <summary> /// Open a database /// </summary> /// /// <returns> /// <see cref="ReturnCode.OK"/> if the DB opened successfully /// </returns> /// /// <param name="filename"> /// Filename of the DB to open or <see cref="MemoryFilename"/> to use RAM /// </param> /// /// <param name="db"> /// Pointer to the opened database /// </param> public static ReturnCode Open(string filename, out Pointer<Database> db) { IntPtr pDb; ReturnCode rc = PInvoke.sqlite3_open(filename, out pDb); db = new Pointer<Database>(pDb); return rc; } /// <summary> /// Close a database /// </summary> /// /// <returns> /// <see cref="ReturnCode.OK"/> if the DB closed successfully /// </returns> /// /// <param name="db"> /// Database to close /// </param> public static ReturnCode Close(Pointer<Database> db) { return PInvoke.sqlite3_close(db.Ptr); } /// <summary> /// Prepare a SQL statement. Call <see cref="Step"/> after this until done /// and then call <see cref="Finalize"/>. /// </summary> /// /// <returns> /// <see cref="ReturnCode.OK"/> if the statement was successfully prepared /// </returns> /// /// <param name="db"> /// DB to prepare the statement for /// </param> /// /// <param name="sql"> /// SQL statement to prepare /// </param> /// /// <param name="statement"> /// The prepared statement /// </param> public static ReturnCode Prepare( Pointer<Database> db, string sql, out Pointer<Statement> statement) { IntPtr ptr; ReturnCode rc = PInvoke.sqlite3_prepare_v2( db.Ptr, sql, -1, out ptr, IntPtr.Zero); statement = new Pointer<Statement>(ptr); return rc; } /// <summary> /// Step the statement to the next result /// </summary> /// /// <returns> /// <see cref="ReturnCode.ROW"/> if stepped to a new result. /// <see cref="ReturnCode.DONE"/> if the statement is finished. /// </returns> /// /// <param name="statement"> /// Statement to step /// </param> public static ReturnCode Step(Pointer<Statement> statement) { return PInvoke.sqlite3_step(statement.Ptr); } /// <summary> /// Finalize a statement when done with it /// </summary> /// /// <returns> /// <see cref="ReturnCode.OK"/> if finalizing was successful /// </returns> /// /// <param name="statement"> /// Statement to finalize /// </param> public static ReturnCode Finalize(Pointer<Statement> statement) { return PInvoke.sqlite3_finalize(statement.Ptr); } /// <summary> /// Execute a SQL query /// </summary> /// /// <returns> /// <see cref="ReturnCode.OK"/> if the query was executed successfully /// </returns> /// /// <param name="database"> /// Database to execute the query on /// </param> /// /// <param name="sql"> /// SQL query to execute /// </param> public static ReturnCode Execute(Pointer<Database> database, string sql) { return PInvoke.sqlite3_exec( database.Ptr, sql, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); } /// <summary> /// Get a text column from the current row of a statement /// </summary> /// /// <returns> /// The text of the given column /// </returns> /// /// <param name="statement"> /// Statement whose current row to get the text of /// </param> /// /// <param name="columnIndex"> /// Index of the column of the statement's current row to get /// </param> public static string GetTextColumn( Pointer<Statement> statement, int columnIndex) { IntPtr ptr = PInvoke.sqlite3_column_text(statement.Ptr, columnIndex); return Marshal.PtrToStringAuto(ptr); } /// <summary> /// Get a 32-bit int column from the current row of a statement /// </summary> /// /// <returns> /// The int value of the given column /// </returns> /// /// <param name="statement"> /// Statement whose current row to get the int value of /// </param> /// /// <param name="columnIndex"> /// Index of the column of the statement's current row to get /// </param> public static int GetIntColumn( Pointer<Statement> statement, int columnIndex) { return PInvoke.sqlite3_column_int(statement.Ptr, columnIndex); } /// <summary> /// P/Invoke wrapper around the native library. See SQLite official docs for /// more about these functions. /// </summary> private static class PInvoke { /// <summary> /// Name of the plugin to load /// </summary> #if UNITY_IOS const string PLUGIN_NAME = "__Internal"; #else const string PLUGIN_NAME = "sqlite3"; #endif [DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern ReturnCode sqlite3_open( string filename, out IntPtr ppDb); [DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern ReturnCode sqlite3_close(IntPtr pDb); [DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern ReturnCode sqlite3_prepare_v2( IntPtr pDb, string sql, int sqlLength, out IntPtr ppStmt, IntPtr pzTail); [DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern ReturnCode sqlite3_step(IntPtr stmt); [DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern ReturnCode sqlite3_finalize(IntPtr stmt); [DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern ReturnCode sqlite3_exec( IntPtr pDb, string sql, IntPtr callback, IntPtr userdata, IntPtr errmsg); [DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr sqlite3_column_text(IntPtr stmt, int iCol); [DllImport(PLUGIN_NAME, CallingConvention = CallingConvention.Cdecl)] public static extern int sqlite3_column_int(IntPtr stmt, int iCol); } }
Usage Example
Now that we have the Sqlite
wrapper class, let’s try it out in a more complete example:
using System.IO; using UnityEngine; public class TestScript : MonoBehaviour { void Start() { // Delete any existing database const string path = "/path/to/highscores.db"; if (File.Exists(path)) { File.Delete(path); } // Open the database Sqlite.Pointer<Sqlite.Database> db; Sqlite.ReturnCode rc = Sqlite.Open(path, out db); if (rc != Sqlite.ReturnCode.OK) { print("Error opening DB: " + rc); return; } // Create the table and insert a few rows rc = Sqlite.Execute( db, "CREATE TABLE HIGHSCORES(" + " ID INT PRIMARY KEY NOT NULL," + " NAME TEXT NOT NULL," + " SCORE INT NOT NULL);" + "INSERT INTO HIGHSCORES(ID, NAME, SCORE)" + " VALUES (1, 'Jackson', 5000);" + "INSERT INTO HIGHSCORES(ID, NAME, SCORE)" + " VALUES (2, 'John', 3000);"); if (rc != Sqlite.ReturnCode.OK) { Sqlite.Close(db); print("Error populating table: " + rc); return; } // Start querying the rows Sqlite.Pointer<Sqlite.Statement> statement; rc = Sqlite.Prepare( db, "SELECT ID, NAME, SCORE FROM HIGHSCORES;", out statement); if (rc != Sqlite.ReturnCode.OK) { Sqlite.Close(db); print("Error querying table: " + rc); return; } // Loop until we've read all rows while (true) { // Go to the next row rc = Sqlite.Step(statement); if (rc != Sqlite.ReturnCode.ROW) { if (rc != Sqlite.ReturnCode.DONE) { Sqlite.Close(db); print("Error stepping query: " + rc); return; } break; } // Get the row's columns and print them int id = Sqlite.GetIntColumn(statement, 0); string name = Sqlite.GetTextColumn(statement, 1); int score = Sqlite.GetIntColumn(statement, 2); print("ID: " + id + ", Name: " + name + ", Score: " + score); } // Finalize the query rc = Sqlite.Finalize(statement); if (rc != Sqlite.ReturnCode.OK) { Sqlite.Close(db); print("Error finalizing query statement: " + rc); return; } // Close the database rc = Sqlite.Close(db); if (rc != Sqlite.ReturnCode.OK) { print("Error closing DB: " + rc); return; } } }
Using the wrapper has several nice aspects to it. First, there’s no need to know anything about the underlying native API. None of the function names or types are directly exposed. Some of the parameters are omitted from the functions. All of the functions and types are renamed to match the normal C# naming conventions. Everything is strongly-typed, including pointers and return codes. And no “unsafe” code is required here either as the pointer types are just a struct containing another struct: IntPtr
.
Running this little script either in editor, in a standalone build with Mono, or in a standalone build with IL2CPP, we get the same correct result:
ID: 1, Name: Jackson, Score: 5000 ID: 2, Name: John, Score: 3000
Conclusion
Today we’ve wrapped a real-world library without making any modifications to it. We’ve overcome some tricky P/Invoke issues and presented a simple interface for the game to use. It’s even a useful library that, with a little more wrapping following the same formula, could be a real boon to a game project that wants to, for example, go beyond the limits of PlayerPrefs
. Hopefully you’ll find this useful as there are many excellent native libraries out there to wrap!
#1 by Olli N on February 25th, 2019 ·
Wow, this is brilliant! The information about native interop is so spread out across tutorials, blog posts and out-of-date documentation. Thanks for this. Any chance for part 2 about the different options available in DllImport and how to use the memory layout options when passing complex types (https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute?view=netframework-4.7.2)?
#2 by jackson on February 26th, 2019 ·
There’s definitely a lot more to learn about P/Invoke than just what this article covers. I may very well write a followup article to cover some of that ground.
#3 by Matt G on February 25th, 2019 ·
Good article! You should probably discuss the CallingConvention field on DllImport. Is the default of stdcall right for sqlite? I would’ve expected its functions to be cdecl. Getting this incorrect will lead to instability and eventual crashes.
#4 by jackson on February 26th, 2019 ·
Cdecl
does seem more appropriate, but I kept things simple for this article because IL2CPP ignores the calling convention anyhow. Here’sPInvoke.sqlite3_open
:The key line is the call to
il2cpp_codegen_resolve_pinvoke
to get the function pointer to invoke the native function. It’s passingIL2CPP_CALL_DEFAULT
for the calling convention instead ofIL2CPP_CALL_C
forCdecl
. Now here’sil2cpp_codegen_resolve_pinvoke
:The
callingConvention
gets wrapped up into aPInvokeArguments
and passed toil2cpp::vm::PlatformInvoke::Resolve
. Here’s what that looks like:Only
entryPoint
andmoduleName
are ever read out of thePInvokeArguments
. So while it would probably be more correct to specifyCdecl
, it wouldn’t have any effect on the generated C++ that gets executed at runtime.#5 by Kyle on February 16th, 2021 ·
Thanks so much for writing this up! Like Olli said this info is spread around everywhere so it’s good to see someone congeal it all into a real-world example.
If possible, I’d love to see another part of this that dives into other ways of managing wrapped function params. I’m in the middle of working on my own wrapper for a C library right now, and looking around I see a lot of people suggesting building out structs yourself to just use
, as well as defining delegates for use with callback functions (here you just use , would love to see registering a proper callback for ). Generally talking about Marshal as well, as information of that specifically is very hard to find for real world examples.Anyways, thank you so much writing this!