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
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
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.