Objects and companions
A Kotlin object singleton becomes a static C# class: no instance, no constructor, just static members reached directly through the type. A data object nested inside a sealed class hierarchy becomes a sealed subclass instead (see Interfaces, abstract and sealed classes). A companion object's members land as static members on the enclosing C# class.
Kotlin | C# | Notes |
|---|
object
| static class
| singleton; methods are PascalCased and their returns marshalled, exactly like a class method |
data object (in sealed class)
| sealed subclass | with ToString |
companion object | static members | |
Kotlin
A top-level singleton, from test-library/src/nativeMain/kotlin/.../cat/CatRegistry.kt:
object CatRegistry {
private val cats: MutableList<String> = mutableListOf()
fun register(name: String) {
cats.add(name)
}
fun count(): Int = cats.size
fun clear() {
cats.clear()
}
}
A companion object, from test-library/src/nativeMain/kotlin/.../cat/Cat.kt:
class Cat(
name: String,
val lives: Int = 9,
) : Animal(name) {
// ...
companion object {
const val SPECIES: String = "Felis catus"
val defaultBreed: String = "Domestic Shorthair"
fun fromName(name: String): Cat = Cat(name)
}
}
Generated C#
CatRegistry renders as a static class with no handle at all. Each method is PascalCased and routes through the same static-function marshalling as a top-level function: a private [DllImport] extern plus a public wrapper that checks the error out-parameter across the bridge.
public static class CatRegistry
{
[DllImport("test", CallingConvention = CallingConvention.Cdecl, EntryPoint = "catregistry_register_")]
private static extern void Register_native(string name, out IntPtr error);
public static void Register(string name)
{
Register_native(name, out IntPtr error);
if (error != IntPtr.Zero)
{
throw NugetErrorNative.BuildException(error);
}
}
[DllImport("test", CallingConvention = CallingConvention.Cdecl, EntryPoint = "catregistry_count")]
private static extern int Count_native(out IntPtr error);
public static int Count()
{
int result = Count_native(out IntPtr error);
if (error != IntPtr.Zero)
{
throw NugetErrorNative.BuildException(error);
}
return result;
}
[DllImport("test", CallingConvention = CallingConvention.Cdecl, EntryPoint = "catregistry_clear")]
private static extern void Clear_native(out IntPtr error);
public static void Clear()
{
Clear_native(out IntPtr error);
if (error != IntPtr.Zero)
{
throw NugetErrorNative.BuildException(error);
}
}
}
A non-primitive return is marshalled to its idiomatic C# type, exactly like a class method — the hidden IntPtr and Marshal.PtrToStringUTF8 live on the generated side, never the consumer's. The Clinic object (test-library/src/nativeMain/kotlin/.../clinic/ClinicSample.kt) has a String-returning method:
object Clinic {
fun greet(name: String): String = "Welcome to the clinic, $name"
fun capacity(): Int = 12
fun reset() {}
}
greet surfaces as a PascalCased Greet returning a real string:
public static class Clinic
{
[DllImport("test", CallingConvention = CallingConvention.Cdecl, EntryPoint = "clinic_greet")]
private static extern IntPtr Greet_native(string name, out IntPtr error);
public static string Greet(string name)
{
IntPtr nativeResult = Greet_native(name, out IntPtr error);
if (error != IntPtr.Zero)
{
throw NugetErrorNative.BuildException(error);
}
return Marshal.PtrToStringUTF8(nativeResult)!;
}
// Capacity() and Reset() follow the same wrapper pattern as CatRegistry above.
}
Object methods thus match class and companion methods on both facets: PascalCased names and marshalled returns (completing ADR-060 cells 1 and 25; naming follows ADR-007).
The Cat companion's members are generated as static members directly on the Cat class itself (Cat.Species, Cat.DefaultBreed, Cat.FromName(...)) rather than a nested type. There's no separate Cat.Companion class in the generated output.
Using it from C#
Singleton object, from IntegrationTests/ObjectTests_Singleton.cs:
[Fact]
public void CatRegistry_IsStaticClass()
{
Assert.True(typeof(CatRegistry).IsAbstract && typeof(CatRegistry).IsSealed);
}
[Fact]
public void CatRegistry_RegisterAndCount()
{
CatRegistry.Clear();
CatRegistry.Register("Oreo");
CatRegistry.Register("Mylo");
Assert.Equal(2, CatRegistry.Count());
CatRegistry.Clear();
}
An object method with a non-primitive return is called just like any other marshalled member — no Marshal.PtrToStringUTF8 at the call site. From IntegrationTests/ObjectMethodMarshallingTests.cs:
[Fact]
public void Clinic_Greet_ReturnsMarshalledString()
{
string greeting = Clinic.Greet("Bob");
Assert.Equal("Welcome to the clinic, Bob", greeting);
}
Companion object, from IntegrationTests/CompanionObjectTests.cs:
[Fact]
public void CompanionConstVal()
{
Assert.Equal("Felis catus", Cat.Species);
}
[Fact]
public void CompanionProperty()
{
Assert.Equal("Domestic Shorthair", Cat.DefaultBreed);
}
[Fact]
public void CompanionFactoryMethod()
{
using var cat = Cat.FromName("Whiskers");
Assert.Equal("Whiskers", cat.Name);
}
Last modified: 21 July 2026