Static classes and methods
A C# static class becomes a Kotlin object with the same name; its static methods become functions and its properties become Kotlin properties on that object. On a non-static C# class, static members instead land in the generated Kotlin companion object.
C# | Kotlin |
|---|
public static class MimeUtility
| internal object MimeUtility
|
public static string Describe(int value)/Describe(bool value)
| fun describe(value: Int): String/describe(value: Boolean)
|
public static string DefaultName { get; set; }
| var defaultName: String
|
public static int RenderCount { get; }
| val renderCount: Int
|
The generated object is internal, not public: it's visible anywhere else in the same Gradle module (including hand-written sources like test-library's own Kotlin files) but invisible to the forward-direction KSP exporter's public-API scan, so a reverse-bound type never accidentally gets re-exported into the packed nupkg's own Interop.cs.
The MimeMapping round trip
test-library/build.gradle.kts binds the MimeMapping package:
nuget {
dependencies {
dependency("MimeMapping", version = "4.0.0") {
bind {
packageName = "mimemapping"
include("MimeMapping")
}
}
}
}
The bound C# API is a static method:
namespace MimeMapping {
public static class MimeUtility {
public static string GetMimeMapping(string file);
}
}
nugetGenerateBindings renders this straight into an object (test-library/build/nuget-interop/kotlin/nativeMain/mimemapping/MimeUtility.kt, real generated output):
internal object MimeUtility {
fun getMimeMapping(file: String): String {
val fn = requireNotNull(MimeUtilityBindings.getMimeMapping__cb4c202351abfeec4d85fccb5ca462a0Fn) {
NugetRegistry.notRegistered("MimeMapping.MimeUtility", "MimeMapping")
}
val resultPtr = memScoped { fn.invoke(file.cstr.ptr) }
?: error("MimeUtility.GetMimeMapping returned null, expected a non-null string pointer")
val result = resultPtr.reinterpret<ByteVar>().toKString()
freeManagedString(resultPtr)
return result
}
}
test-library calls it from ordinary Kotlin (test-library/src/nativeMain/kotlin/io/github/xxfast/kotlin/native/nuget/sample/mime/MimeSample.kt):
package io.github.xxfast.kotlin.native.nuget.test.mime
import mimemapping.MimeUtility
fun mimeTypeFor(fileName: String): String = MimeUtility.getMimeMapping(fileName)
And the C# test exercises the whole path end to end (IntegrationTests/MimeRoundTripTests.cs):
[Fact]
public void MimeTypeFor_JsonFile_ReturnsApplicationJson()
{
string result = MimeSample.mimeTypeFor("data.json");
Assert.Equal("application/json", result);
}
That's the full trip: C# test code calls the forward-bridged MimeSample.mimeTypeFor, which is Kotlin code, which calls the reverse-bridged MimeUtility.getMimeMapping, which is itself a thunk call back into the real MimeMapping NuGet package.
Static properties
The bound Test.Text.Template fixture has both property forms:
// TestDependency/Template.cs (real source)
public static string DefaultName { get; set; } = "Oreo";
public static int RenderCount { get; private set; }
Because Template is not a C# static class, its static properties are generated in its companion object:
// build/nuget-interop/kotlin/nativeMain/sample/text/Template.kt (real generated output)
companion object {
var defaultName: String
get() { /* calls defaultNameGetterFn */ }
set(value) { /* calls defaultNameSetterFn */ }
val renderCount: Int
get() { /* calls renderCountGetterFn */ }
}
Each accessor is a call through its registered function pointer. The generated C# thunks access the underlying static property directly:
// build/nuget-interop/csharp/TemplateRegistration.cs (real generated output)
private static IntPtr DefaultName_Get_Thunk()
{
string result = Template.DefaultName;
return Marshal.StringToCoTaskMemUTF8(result);
}
private static void DefaultName_Set_Thunk(IntPtr valuePtr)
{
Template.DefaultName = Marshal.PtrToStringUTF8(valuePtr)!;
}
Hand-written Kotlin uses the companion members through Template itself:
// test-library/src/nativeMain/kotlin/.../sample/Greetings.kt (real source)
fun setDefaultTemplateCatName(name: String): String {
Template.defaultName = name
return Template.defaultName
}
fun templateRenderCount(): Int = Template.renderCount
The consumer-side round trip calls these Kotlin functions through the forward bridge:
// IntegrationTests/TemplateRoundTripTests.cs (real source)
[Fact]
public void StaticProperties_MyloNameAndRenderCount_RoundTripThroughKotlin()
{
string name = Greetings.setDefaultTemplateCatName("Mylo");
int renderCount = Greetings.templateRenderCount();
Assert.Equal("Mylo", name);
Assert.True(renderCount >= 0);
}
Naming and registration
The registration export name is derived from the C# namespace and type name: MimeMapping.MimeUtility → nuget_mimemapping_mime_utility_register. One [UnmanagedCallersOnly] thunk is registered per bridgeable static method; see Consuming C# in Kotlin for the full registration handshake.
Static method overloads
The Test.Overloads.OverloadLab fixture has two Describe methods:
public static string Describe(int value) => $"static:int:{value}";
public static string Describe(bool value) => value ? "static:bool:on" : "static:bool:off";
Generated Kotlin keeps the shared name and lets normal overload resolution select by parameter type:
fun describe(value: Boolean): String {
val fn = requireNotNull(OverloadLabBindings.describe__09da8e80bd8920c59e5252f3d665716aFn) {
NugetRegistry.notRegistered("Test.Overloads.OverloadLab", "TestDependency")
}
val resultPtr = fn.invoke(value)
?: error("OverloadLab.Describe returned null, expected a non-null string pointer")
val result = resultPtr.reinterpret<ByteVar>().toKString()
freeManagedString(resultPtr)
return result
}
fun describe(value: Int): String {
val fn = requireNotNull(OverloadLabBindings.describe__a8ac9b64f5e802cd2f6fdcaa8b7dc202Fn) {
NugetRegistry.notRegistered("Test.Overloads.OverloadLab", "TestDependency")
}
val resultPtr = fn.invoke(value)
?: error("OverloadLab.Describe returned null, expected a non-null string pointer")
val result = resultPtr.reinterpret<ByteVar>().toKString()
freeManagedString(resultPtr)
return result
}
Limitations
Static properties support the current bridgeable primitive, string, and handle vocabulary. A handle-typed static property with a setter now renders as a Kotlin var too, its type driven by the property's own NullableAttribute (see Instance members and ADR-053).
Unsupported overloads are diagnosed independently. A true mapped Kotlin-signature collision is a generation error; see The bridgeable subset.
Last modified: 21 July 2026