Kotlins Unterstützung für verschiedene Plattformen erlaubt das Interoperieren mit Programmiersprachen für diese Plattformen. Dazu gibt es plattformspezifische Varianten der Sprache und der SDK. Mit Kotlin/JVM ließe sich mit JVM-Sprachen wie Java interoperieren. Mit Kotlin/Native ließe sich mit Sprachen wie C interoperieren. Es gibt noch mehr, aber kein Kotlin/.NET. Wie könnte man eine Kotlin-Bibliothek in einem .NET-Programm aufrufen?

Durch die Wahl von Kotlin/JVM könnte man sich mittels JNI einen Weg ebnen. Ein anderer Weg geht über Kotlin/Native, womit sich eine shared library (DLL) bauen läßt, die das .NET-Programm dann laden und aufrufen kann. Diesen Weg behandelt der folgende Text.

Zu diesem Zweck schrieb ich eine Kotlin-Bibliothek mit einigen Klassen


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
}


die Zahlen addieren und subtrahieren können. Die Funktion Callback.call hat einen Funktionspointerparameter. Dieser muss in einer interop.def-Datei

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

im Kotlin/Native-Projekt deklariert werden. Bauen des Projekts auf Windows erstellt eine math.dll zusammen mit einer Headerdatei math_api.h. Die DLL hat eine Funktion, die eine Struktur mit einigen Funktionspointern


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);


zu den eigentlichen Funktionen aus der Kotlin-Bibliothek enthält. Darin spiegeln sich die Namensräume, Klassen und Funktionen der Kotlin-Bibliothek. In C läßt sich das struct dank des '.'-Operators (struct member operator) relativ einfach nutzen. In C# hingegen ist das nicht so einfach. Einfacher sollte es mit dem Parser werden, den ich zusammen mit einem dynamischen Bindungsmechanismus in der .NET-Bibliothek KotlinNative2Net geschrieben habe. Damit lassen sich die Funktionen der obigen Kotlin/Native-Bibliothek aus C# 10 heraus wie folgt aufrufen.


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");
}


Starten des Programs schreibt

2 + 3 = 5
2 - 3 = -1
Add 2 and 3 in .NET.
2 + 3 = 5
(π - 1) + 1 = 3,141592653589793
Bye.
zur Standardausgabe.

Zu Beginn werden die Pfade zum Header math_api.h. und zur DLL math.dll benötigt. Ich kopierte beide in den Ausgabeordner der .NET-Anwendung um mit dem zur Laufzeit bekannten Ort der .NET-Assembly die Pfade setzen zu können. Dieser Weg erlaubt es den '.'-Operator zu nutzen um durch das struct aus obigem Header zu navigieren und die Funktionen darin aufzurufen. Dazu ist für jeden Funktionstyp „Klebercode“ erforderlich um die Aufrufe zuordnen und durchführen zu können. Ein Beispiel für Funktionen, welche double zurückgeben und drei Parameter (pointer, double, double) haben, ist in PtrDoubleDouble_DoubleInvoker implementiert. Dieser Kleber ist für die Funktion root.arithmetic.FloatPlus.FloatPlus.add nötig, die im Beispiel gerufen wird. Die Sequenz Invokers.Default enthält Kleber für die restlichen Funktionstypen.

Der Weg ist gangbar. Ein Nachteil ist das umständliche schreiben des Klebercodes. Ferner könnte sich die Ausgabe von Kotlin/Native, also der Header und die DLL, in Zukunft ändern. Zumindest laut Doku. Kotlin/Native exports symbols is subject to change without notice according to its documentation. Dadurch könnte der Weg zu Bruch gehen. Davon abgesehen müsste KotlinNative2Net auch für andere Kotlin/Native-Bibliotheken funktionieren, vorausgesetzt der Parser parst den Header korrekt und der notwendige Klebercode wird injiziert. Wenn sie eine Nutzung für ihr Kotlin/Native-Projekt in Betracht ziehen, sollten sie damit rechnen, Klebercode schreiben und den Parser verbessern zu müssen.