Sharp Objects

Step 10 - Sharp Objects

So someone asked, can you dynamically load the library in csharp just like python? Hmm. Probably....

That would be nice, and I'm pretty sure we can do a lot of manipulation on the go with C#, like we can in python, there's a comprehensive reflection and invocation interface. How hard can it be? Can we build a c# interface on the fly instead of outputting source code to be compiled?

so, significant rummaging later and the answer is a definite "sort of". 👍😁

Emit emit emit

Initially, I used Reflection.Emit to generate CLR code at runtime... this worked and provided a fully dynamic binding doing the same as Python, loading the C++ library, calling our C API to interrogate it and building matching C# CLR objects at runtime. yay great success, move on....

but, developing with it is "challenging" you need to know all the available functions and types, write all the code wire up a command line build and crank the handle. So possibly useful for some sort of "auto-generated" use, but not great for interactive development.

bother. So plan B.

Dynamic path - DynamicObject

First up, we can use a simpler trick with a DynamicObject that intercepts every method call and forwards it straight to XPlatRuntime.Call.

dynamic lib  = XPlatModule.Load("libdemo.so");
using dynamic demo = lib.Demo();    // a DynamicInstance
Console.WriteLine(demo.getInt());   // 42

XPlatModule.Load is the runtime entry point. It loads the library, reads the registry via MetaReader, and registers all method signatures, struct field types and enum values with XPlatRuntime so the marshaling layer knows how to convert every argument.

The downside is everything is dynamic - you get red squiggles everywhere, no (useful) intellisense, and any failures are runtime rather than compile time.

but....

Enter Roslyn

Visual Studio has the Roslyn generator, this can be configured in the projects so that at build time, and continuously within the IDE it can execute our code, we can take advantage of that to detect using our binding library, build the metadata and output generated C# source into the dev environment. This happens in the background, and at that point our IDE knows all of our names and types, it should feel just like native development.

So there's some wiring required, but in our solution we'll specify our native libraries as AdditionalFiles and add our new XPlatBindingGenerator as an "Analyzer" project in the references. This way our generator watches for any "AdditionalTextsProvider" lines for a native library, once found we build our metadata model of the native library as LibModel through the C API. The code emitter then walks the model and creates C# source for the library e.g. for demo.dll it will output XPlatDemo.gen.cs

The Init code in XPlatDemo is annotated with [ModuleInitializer] which will cause it to get invoked by the NET runtime before Main.

This way it just works like a normal assembly, we can add a using and treat it just like a normal .NET assembly:

using XPlatDemo;                  // our generated stub
using var demo = new Demo();      // A real C# object
Console.WriteLine(demo.getInt()); // 42

all the classes, methods and types look like normal C# types. and we have intellisense.

P/Invoke - how we call the C API

Java has JNA to bridge to native code, C# has P/Invoke. Using P/Invoke we could use [DllImport] but that's not quite flexible enough especially since we're trying to make the code cross platform and we might need to change the search paths for the binding, so instead delegate pointers will be used.

At the end of the day the usage is pretty similar, firstly ewach different signature gets a delegate type:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate IntPtr  D_Ptr_PS(IntPtr a, UIntPtr b);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate UIntPtr D_UInt_PP(IntPtr a, IntPtr b);

Then at runtime NativeLibrary.GetExport resolves each symbol and the Marshal.GetDelegateForFunctionPointer wraps it in a strongly-typed callable.

Which results in a callable C# delegate:

var className = api.GetClassName(registry, (UIntPtr)i);

Usage

There's still some reorganising to do to make the final solution simple to integrate into generic projects e.g. wiring the native library info in a config file etc., but, for the demo, once the native libraries are built either open the VisualStudio solution and run the DemoApp.... or from the command line enter the DemoApp directory and run: dotnet run

$ dotnet run
Using launch settings from xplat/csharp/DemoApp/Properties/launchSettings.json...
Basic types
getBool:   False
getInt:    42
getLong:   1234567890
getDouble: 3.14
getString: hello
getEnum:   Red
getDate:   26/11/1971

🎉 party time. excellent.

(code is in codeberg - codeberg)