Classes and objects
A Kotlin class becomes a C# class implementing IDisposable. Each instance wraps an opaque native handle: dispose it when you are done, and every constructor call, property access, and method call crosses the bridge through that handle.
Kotlin | C# |
|---|---|
|
|
constructor |
|
| property (get / get+set) |
nested | nested C# type, see Nested types |
Object identity and disposal
An object-typed property or method return mints a new C# wrapper on every access; there is no identity caching, so reading oreo.Brother twice gives wrappers that are NotSame but Equal. Disposal does not cascade: a wrapper's Dispose() releases only its own handle. Disposing a Brother wrapper you read off oreo does not affect oreo, and does not affect any other wrapper read from the same property. Dispose every wrapper you hold, including ones you only read a property or method return into.
Method returns
An instance method returning an object, a nullable type, a collection, an enum, or Char marshals the same way a property of that type would (see Collections and Enums). A method returning a nullable primitive, including Boolean?, evaluates only once: unlike a property getter it may have side effects, so it can't be called twice to fetch a has-value flag and then the value.
A nullable class handle parameter
A parameter typed with a nullable exported class (Foo?), on a constructor, method, extension, or top-level function, binds as an ordinary nullable handle: null needs no separate has-value flag, since a handle already has its own null. The handle is borrowed, not consumed: the same instance can back several calls and stays usable afterwards.
A nullable return-shaped type at a parameter (Flow<Event>?) is a different case; see Publishing Kotlin to C#.
Method overloads
Two or more same-named methods on an exported class collapse into one natural C# overload set; call whichever one matches your argument types.
C# cannot overload on reference nullability alone: a pair like fun tag(s: String) and fun tag(s: String?) would render two identical C# signatures, so generation fails outright (ERROR_CSHARP_SIGNATURE_COLLISION) instead of producing invalid C#. Give one of them a distinct name.
A class's own interface beside a kept base class
A class with an exported base class keeps its own exported interfaces too: class X : Base(), IFoo renders public class X : Base, IFoo, not Base alone with IFoo dropped. See Interfaces, abstract classes, and sealed classes for when a base class or interface is dropped instead of kept.
Constructor default parameters
Each exported constructor gets one additional C# overload per maximal trailing run of defaulted parameters, omitting that suffix; Kotlin supplies the omitted arguments, evaluated on the Kotlin side. A default followed by a required parameter (a middle default) produces no overload, since a positional call can't skip over it.
Carrier gets three public constructors: the full signature, one omitting padded, and one omitting both padded and size. Kennel's capacity sits before the required city, so nothing is synthesized: exactly one public constructor.
A trailing default the bridge cannot carry costs only the arities that still have it.
The constructor that would still take events does not exist.
Generated C#
Every constructor overload, synthesized or not, carries the same out IntPtr error shape as any other constructor call:
A constructor overload that collides with another, declared or synthesized, fails generation with the same ERROR_CSHARP_SIGNATURE_COLLISION diagnostic used for method overloads, naming the defaulted parameter as the cause.
Method default parameters
The same rule extends to class methods, and to object /companion members, top-level functions, and extension functions, each with its own overload-numbering scope; see Objects and companions, Top-level declarations, and Extensions.
A trailing default the bridge cannot carry costs only the overloads that still have that parameter. Shorter fully-bridgeable overloads still bind; the unsupported arity stays a named skip.
desk.Open(settings, events) does not exist. The build still warns SKIPPED_UNSUPPORTED_INPUT naming events. The same cut applies to constructors, top-level functions, object and companion members, and extensions.
An override does not synthesize its own omitting overload when its C# base already carries one: the override just inherits it through ordinary C# inheritance. When there is no C# base to inherit from, or the member comes through an interface, no omitting overload is generated; call the override with every argument.
No public constructor
A class whose every public constructor is skipped, for example because a constructor parameter has an unsupported type, is still exported: it just has no public C# constructor, only the internal one every handle class carries. C# code obtains an instance from a Kotlin factory function or property that returns one. The build warns, naming the skipped constructor and the reason, and the same detail appears as an XML <remarks> comment on the generated class, so it shows up as an IDE tooltip too.
Classes declared in a dependency module
A class does not need to be declared in the publishing Gradle module to reach the generated C# API. Starting from the module's own exported declarations, the processor also walks return, parameter, and property types, and admits any type it discovers in a dependency module, under that type's own Kotlin package rather than the exporting module's. See The nuget {} DSL for the include/exclude rule this follows.
Nested types
A public class, object, interface, or enum class, at any depth, declared inside a non-generic, non-inner class or object, an interface, or a sealed base/arm, becomes a real C# nested type, Outer.Nested.
Kotlin nested kind | C# shape |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
An inner class's own nested types (inner-of-inner), a generic owner, and an enum class owner have no C# equivalent for a nested slot and stay a named skip (SKIPPED_NESTED_DECLARATION); see Inner classes for what an inner class itself declares. Kotlin allows a nested type named exactly like its owner, or like a PascalCased member of its owner (a companion's members included, since they fold into the owner's C# type as statics); C# does not, so that combination fails generation (ERROR_CSHARP_SIGNATURE_COLLISION) instead of emitting invalid C#. Avoid naming an accessor, or a companion function, the same as its nested return type (fun perch(): Perch); name it differently instead (perchAt).
inner class: the constructor takes the outer instance first
A public Kotlin inner class declared directly inside an admitted, non-generic, non-inner class becomes a nested C# type too, with one difference from a plain nested class: its constructor's first parameter is the outer instance, named outer.
Disposing hearth before sunbather is safe: the inner instance's own reference to its outer keeps the Kotlin object alive, so sunbather.Basking keeps reading it after hearth.Dispose() runs. A declared constructor parameter literally named outer renders as outer_ in both the generated C# signature and the Kotlin export instead (the same shift value already gets on a property setter); a member reading this@Hearth needs nothing added at the ABI, since the reference lives entirely on the Kotlin heap.
An inner class as an owner of its own nested types (inner-of-inner), an inner class under a sealed owner, a generic inner class, and an inner class of a generic outer stay a named skip (SKIPPED_NESTED_DECLARATION).
A @Serializable class exports the same as any other class. kotlinx.serialization's compiler-generated $serializer nested object is never declared in C#, since $ isn't a legal C# identifier there; it has no members you'd want to call, so skipping it costs nothing. Nothing about it appears in the build log either: a member the compiler wrote, a hidden-deprecated member or the serialization plugin's serializer(), is never reported as skipped.
An object at a member position stays CS0722
A Kotlin object, nested or top-level, renders as a C# static class, and C# forbids a static type at a parameter or return position (CS0722). A member typed with one is skipped rather than emitted as uncompilable C# (SKIPPED_UNSUPPORTED_TYPE); the object type itself is still declared, so call its members directly instead of routing through the skipped member.
Owners ADR-134 admits
An interface owner, a sealed base/arm owner, and a nested value class candidate under any admitted owner all declare nested types too:
The owner-name collision above is checked against the generated C# name, not the Kotlin one: an interface owner gets an I prefix in C#, so a nested type sharing its interface owner's Kotlin name is legal (interface Cage { class Cage } declares ICage.Cage, two different C# names, not a collision).
A sealed arm's internal Arm(IntPtr handle) constructor still exists beside any exported public one. Purr.On's own level: Int constructor parameter is bridgeable, so On also exports a public constructor, and new Purr.On(9) resolves to it rather than to the internal one; see Interfaces, abstract classes, and sealed classes: Sealed classes and interfaces for the general rule and its refused-arm exception.
Limitations
Map/Setare not yet supported as method or constructor parameters; see Collections.Value classes don't get default-parameter overloads on their constructor or methods, and
Copy(...)can't omit arguments.An
overridewith no exported C# base to inherit an omitting overload from, or a member reached through an interface, gets no synthesized overload; call it with every argument. An override on a sealed class arm never gets one either, even when its sealed base has none to inherit.An exported Kotlin interface doesn't number same-named methods at all; avoid declaring an overload directly on an interface.
Defaults on an
expect/actualclass member are only synthesized for the top-level-function route and the primary constructor; a class method, companion member, extension, or secondary constructor on anexpect/actualclass gets no synthesized overload. See expect/actual declarations.A nested type under an owner other than the shapes covered above stays a named
SKIPPED_NESTED_DECLARATIONskip. A member typed with a nestedvalue classunder a still-deferred owner (a generic orenum classowner) skips named too (SKIPPED_UNSUPPORTED_TYPE, reasonUNDECLARED_VALUE_CLASS) instead of emitting an unusable struct name; move the value class to the top level of its file, or to an admitted owner, to bridge it. An extension function or property on a nested type is not supported; declare the extension on a top-level type instead.