Kotlin supports multiple platforms through platform-specific versions of the language. This can be leveraged to interoperate with other programming languages for these platforms. There is Kotlin/JVM to interoperate with code written in JVM languages such as Java. Also, there is Kotlin/Native to interoperate with code written in languages such as C. There are more. But there is no Kotlin/.NET to interoperate with .NET languages. How to call a Kotlin library from .NET?

One way to call a Kotlin library from .NET is to pick Kotlin/JVM and to call it through JNI . Another way is to pick Kotlin/Native, build a shared library and to load and call that from .NET. I had a look a the latter way.

To that end, I wrote a Kotlin library with a few classes


package arithmetic

import kotlinx.cinterop.*
import interop.*

class Plus(private val a: Int, private val b: Int) {
    fun add(): Int {
        return a + b
    }
}

class Minus(private val a: Int, private val b: Int) {
    fun subtract(): Int {
        return a - b
    }
}

class Callback() {
    fun call(f: IntInt_Int, a: Int, b: Int): Int = f(a, b)
}

class FloatPlus() {
    fun add(a: Double, b: Double) = a + b
}


that can add and substract numbers. The Callback.call has a function pointer parameter that has to be declared in an interop.def file with the content

---
typedef int  (*IntInt_Int)(int, int);

in the Kotlin/Native project. Building the project on Windows emits a math.dll along with a header file math_api.h. The shared library has a function to get a struct of function pointers


typedef struct {
  /* Service functions. */
  void (*DisposeStablePointer)(math_KNativePtr ptr);
  void (*DisposeString)(const char* string);
  math_KBoolean (*IsInstance)(math_KNativePtr ref, const math_KType* type);
  math_kref_kotlin_Byte (*createNullableByte)(math_KByte);
  math_kref_kotlin_Short (*createNullableShort)(math_KShort);
  math_kref_kotlin_Int (*createNullableInt)(math_KInt);
  math_kref_kotlin_Long (*createNullableLong)(math_KLong);
  math_kref_kotlin_Float (*createNullableFloat)(math_KFloat);
  math_kref_kotlin_Double (*createNullableDouble)(math_KDouble);
  math_kref_kotlin_Char (*createNullableChar)(math_KChar);
  math_kref_kotlin_Boolean (*createNullableBoolean)(math_KBoolean);
  math_kref_kotlin_Unit (*createNullableUnit)(void);

  /* User functions. */
  struct {
    struct {
      struct {
        struct {
          math_KType* (*_type)(void);
          math_kref_arithmetic_Callback (*Callback)();
          math_KInt (*call)(math_kref_arithmetic_Callback thiz, void* f, math_KInt a, math_KInt b);
        } Callback;
        struct {
          math_KType* (*_type)(void);
          math_kref_arithmetic_FloatPlus (*FloatPlus)();
          math_KDouble (*add)(math_kref_arithmetic_FloatPlus thiz, math_KDouble a, math_KDouble b);
        } FloatPlus;
        struct {
          math_KType* (*_type)(void);
          math_kref_arithmetic_Minus (*Minus)(math_KInt a, math_KInt b);
          math_KInt (*subtract)(math_kref_arithmetic_Minus thiz);
        } Minus;
        struct {
          math_KType* (*_type)(void);
          math_kref_arithmetic_Plus (*Plus)(math_KInt a, math_KInt b);
          math_KInt (*add)(math_kref_arithmetic_Plus thiz);
        } Plus;
      } arithmetic;
    } root;
  } kotlin;
} math_ExportedSymbols;
extern math_ExportedSymbols* math_symbols(void);


to the actual user functions. The struct mirrors the namespaces, the classes and the functions in the Kotlin code. This is easy to consume in C with the structure member operator. In C# it is not so easy. In an attempt to make it easy, I wrote a parser and a dynamic binding mechanism in a library named KotlinNative2Net . With KotlinNative2Net the functions in Kotlin/Native library above can be called in C# 10 as follows.


using System.Runtime.InteropServices;
using KotlinNative2Net;
using static System.Console;
using static System.Math;

unsafe
{
    string assemblyLocation = System.Reflection.Assembly.GetExecutingAssembly().Location;
    string apiPath = Path.GetDirectoryName(assemblyLocation) + Path.DirectorySeparatorChar + "math_api.h";
    string sharedLibPath = Path.GetDirectoryName(assemblyLocation) + Path.DirectorySeparatorChar + "math.dll";

    using KLib kLib = (KLib)KLib.Of(apiPath, sharedLibPath, Invokers.Default.Add(new PtrDoubleDouble_DoubleInvoker()));
    dynamic dynKLib = kLib;

    dynamic plus = dynKLib.root.arithmetic.Plus.Plus(2, 3);
    int five = plus.add<int>();
    WriteLine($"2 + 3 = {five}");

    dynamic minus = dynKLib.root.arithmetic.Minus.Minus(2, 3);
    int minusOne = minus.subtract<int>();
    WriteLine($"2 - 3 = {minusOne}");

    dynamic callback = dynKLib.root.arithmetic.Callback.Callback();
    delegate* unmanaged<int, int, int> netAdd = &NetAdd;
    int netAddResult = callback.call<int>((IntPtr)netAdd, 2, 3);
    WriteLine($"2 + 3 = {netAddResult}");

    // This uses the PtrDoubleDouble_DoubleInvoker below.
    dynamic floatPlus = dynKLib.root.arithmetic.FloatPlus.FloatPlus();
    double pi = floatPlus.add(PI - 1, 1d);
    WriteLine($"(π - 1) + 1 = {pi}");

    plus.Dispose();
    minus.Dispose();
    callback.Dispose();
    floatPlus.Dispose();
    WriteLine("Bye.");

    [UnmanagedCallersOnly]
    static int NetAdd(int a, int b)
    {
        WriteLine($"Add {a} and {b} in .NET.");
        return a + b;
    }
}

delegate double PtrDoubleDouble_Double(IntPtr kObj, double a, double b);

class PtrDoubleDouble_DoubleInvoker : Invoker
{
    public override object? Invoke(KLib kLib, KFunc func, IntPtr kObj, object?[] args)
    => kLib
        .GetFunc<PtrDoubleDouble_Double>(func)
        .Map<double?>(d => d(kObj, (double)args[0], (double)args[1]))
        .IfNoneUnsafe(() => null);

    public override bool IsMatch(KFunc func, object[] args)
    => 2 == args.Length
        && func.Params.Skip(1).All(x => x.Type.EndsWith("KDouble"))
        && func.RetVal.Type.EndsWith("KDouble");
}


Running the app prints

2 + 3 = 5
2 - 3 = -1
Add 2 and 3 in .NET.
2 + 3 = 5
(π - 1) + 1 = 3,141592653589793
Bye.
to the console.

To start, the paths to the header math_api.h and to shared library math.dll are required. Here, I copied both to the output directory of the .NET app, hence I could use the directory of the executing .NET assembly to construct the paths. The approach allows to use the structure member operator in C# to call the functions declared in struct in the header. For dispatching the function calls, glue code is required for every function type. The glue is passed to KLib.Of. The Invokers.Default has glue for all function types required in the sample above except one. The missing one is implemented in PtrDoubleDouble_DoubleInvoker. It is required for functions returning a double and having three parameters (pointer, double, double) such as root.arithmetic.FloatPlus.FloatPlus.add.

The approach works. On the flip side, writing glue code is awkward. Furthermore, the way Kotlin/Native exports symbols is subject to change without notice according to its documentation. And breaking changes without notice can break things without notice. Without such breaking changes KotlinNative2Net may work for other Kotlin/Native libraries also given the parser parses the header correctly and the required glue code is injected. You may have to improve the parser and inject glue to make it work for your Kotlin/Native project.