kotlin-native-nuget Help

The bridgeable subset

Not every public C# type or member crosses into Kotlin. This page is the support matrix: what binds, what shape it takes in Kotlin, and what to do when a build warning says a member was skipped.

What binds

C# construct

Binds as

Notes

Public class with at least one bridgeable member

Kotlin class, handle-backed

see Objects and handles

Public static class

Kotlin object

see Static classes and methods

Public, top-level, non-generic interface with at least one admissible member

Kotlin interface plus a wrapper for interface-typed values

see Interfaces below

Public, top-level, default-int-backed, non-[Flags] enum with unique contiguous values 0..N-1

standalone Kotlin enum class

see Enums below

Bridgeable struct (a state-covering constructor, or public settable fields/auto-properties)

immutable Kotlin data class, never a handle

see C# structs

Generic class reached through a closed instantiation (Box<int>)

real Kotlin generic class, one per instantiation

see Generic types

record class

same rules as an ordinary class; not a distinct construct in metadata

ref struct (Span<T>, ReadOnlySpan<T>, a custom one)

not bound

any member referencing one is skipped

Nested type, public or not

not bound

only top-level public types are candidates

Open generic type parameter

member skipped

Generic interface (IBox<T>)

not bound

Enums

A supported enum becomes a standalone Kotlin enum class. Member names convert to SCREAMING_SNAKE_CASE. Enum values cross the ABI as their ordinal Int, so a constructor argument, property, or return typed with the enum uses the generated Kotlin type directly:

// TestDependency/CatMood.cs public enum CatMood { Playful, Sleepy, Hungry, }
enum class CatMood { PLAYFUL, SLEEPY, HUNGRY }
  • [Flags] enums, a non-int underlying type, and explicit, sparse, negative, or aliased values don't bind.

  • A nullable enum, a nested enum, and an enum as a collection element (List<CatMood>) aren't supported yet. An enum used as the type argument of a bound generic class (Box<CatMood>) is supported.

  • An unsupported enum is skipped, named on a build warning (see Build warnings below), not silently dropped.

Interfaces

A public, top-level, non-generic interface with at least one admissible member becomes a plain Kotlin interface:

// TestDependency/Menagerie.cs public interface IFeedable { string Describe(); int Legs { get; } void Feed(string food); string? Nickname { get; set; } }
interface IFeedable { fun describe(): String fun feed(food: String) val legs: Int var nickname: String? }

The interface itself carries no handle. A value arriving at an interface-typed position (a parameter, a return, or a property) is wrapped in a generated class that implements the interface and dispatches every member back to the real C# object, whether or not that object's own runtime class is itself bound.

Interface inheritance binds too: interface ITagged : IFeedable becomes interface ITagged : IFeedable in Kotlin, and dispatches IFeedable's inherited members correctly, as long as IFeedable is itself admissible and bound. If the base isn't, the derived interface binds with only its own declared members. A bound class only declares an implemented interface as a Kotlin supertype when every interface member has an identically-signed public member on the class; a C# explicit interface implementation is invisible in metadata, so such a class omits the supertype entirely.

Implementing a C#-declared interface in Kotlin

A plain Kotlin class implementing IFeedable, with no generated wrapper machinery of its own, can be passed back at any IFeedable-typed parameter or property:

private class Goat : IFeedable { var meals: Int = 0 private set override fun describe(): String = "Nibbles the goat" override val legs: Int get() = 4 override fun feed(food: String) { meals++ } override var nickname: String? = null }

Nothing needs to be registered or generated ahead of time: the first time a Goat crosses into C#, Kotlin mints a small C#-side bridge for it automatically. From C#, the result is indistinguishable from a bound IFeedable:

// IntegrationTests/MenagerieRoundTripTests.cs string result = MenagerieSample.KotlinGoatIntroduce(); Assert.Equal("introduced Nibbles the goat with 4 legs", result);

Only a limited set of member shapes can cross this bridge: val/var property getters and setters, and methods of arity 0-2 returning Unit, a primitive, Boolean, an enum, String, String?, a bound-object handle, or a bound interface (nullable included for the last two). A struct-typed member, a generic-instance-typed member, a collection-typed member (List/Map/Set), or a Task-returning member is out of scope and named on a build warning instead of bridged; a class with one of those members can't be passed at that interface position.

A Kotlin implementation of a derived interface binds too: Tabby : ITagged (where ITagged : IFeedable) can be passed at either an ITagged-typed or an IFeedable-typed C# parameter. A C# object handed into a Kotlin interface-slot parameter transfers ownership to Kotlin, so an implementation can safely store it past the call that delivered it, the same as an ordinary reverse-bound parameter.

Exposing a C# interface in your own Kotlin API

A bound C# interface can also appear at an ordinary forward parameter or return, on a Kotlin author's own public API, using the original C# type:

// Farm.kt class Farm { private var resident: IFeedable? = null fun adopt(feedable: IFeedable) { resident = feedable } fun resident(): IFeedable = resident ?: error("Farm.resident() called before Farm.adopt()") }

The generated C# signature names the real Test.Menagerie.IFeedable, not a re-projected duplicate, so it composes with the rest of that package's own API. A parameter is an ordinary handle transfer, so a C#-implemented value passed in and returned back out is the same managed instance, and a Kotlin implementation passed in resolves to the original Kotlin object:

// IntegrationTests/BidirectionalTests.cs var farm = new Farm(); var goat = new CSharpGoat(); farm.Adopt(goat); Assert.Same(goat, farm.Resident());

This works even for a consumer that never directly depends on the package IFeedable came from: your library's own package metadata declares it as a transitive dependency.

Supported today: a non-nullable bound interface at an ordinary parameter or a method/top-level function return. Not supported, each named on a build warning instead of silently skipped:

  • A nullable interface-typed position (IFeedable?).

  • An interface-typed property.

  • An interface as a collection component (List<IFeedable>).

  • A bound class at a forward position (fun sanctuary(): Sanctuary): not attempted at all.

  • A return of a Kotlin implementation of an interface that has no C#-side bridge for it (see Implementing a C#-declared interface in Kotlin above); the same interface still works as a parameter.

  • An interface stays unavailable at a forward position at all if it, or a base it extends, has a member that references an internal bound class: a public interface referencing an internal type wouldn't compile.

Interface limitations

  • Generic interfaces (IBox<T>) don't bind.

  • A static, static abstract, or static virtual interface member is skipped.

  • A default interface method is skipped.

  • An interface with zero admissible members is skipped entirely.

  • No downcast from an interface-typed value to a concrete bound class (see the note above).

Methods and properties

Static and instance methods and properties bind on classes the same way; see Instance members and Static classes and methods. Struct members bind under their own rules; see C# structs.

  • async/Task-returning methods (Task, Task<T>, ValueTask, ValueTask<T>, IAsyncEnumerable<T>) don't bind. This applies on classes and interfaces alike.

  • An indexer (this[int]) doesn't bind, on a class or an interface.

  • An event member doesn't bind, on a class or an interface.

Overload sets

Bridgeable C# method and constructor overloads keep their name and parameter types as ordinary Kotlin overloads. An unsupported member is diagnosed independently and doesn't hide supported siblings. If two different C# signatures would collapse to the same Kotlin scope, name, and ordered parameter types, generation fails with an error naming both signatures instead of producing a broken build.

Types that cross the wire

Beyond primitives and string (see Primitives and strings; the reverse direction has no nullable value types and no Instant/Duration/Uuid mapping), a member's type binds as:

  • a bound, non-static, non-value-type, non-ref struct class from the current extraction: an opaque handle, see Objects and handles

  • a bridgeable struct: a decomposed, handle-free data class, see C# structs

  • a supported enum: an ordinal Int converted to and from a Kotlin enum class, see Enums above

  • a bound, admissible interface: a Kotlin interface, see Interfaces above

Everything else, arrays, collections, delegates, dynamic, object, open generics, and a generic instantiation of a definition outside the bound assemblies (List<int>), doesn't bind. A closed instantiation of a bound generic class is the one generic shape that does; see Generic types. System.String is the only external (out-of-assembly) reference type recognized: a type from a namespace you didn't include(), from an assembly outside the extraction run, or from an undeclared NuGet dependency is treated the same as an unsupported type, even though the reader can see and name it.

Exceptions

A C# exception thrown while dispatching a bound member is caught at the crossing and reaches Kotlin as a catchable NugetManagedException instead of terminating the host:

try { infirmary.temperature("Oreo") } catch (e: NugetManagedException) { e.managedType // "System.ArgumentException" e.message // "Oreo is not a registered patient" }

NugetManagedException carries the .NET type's full name and its Message, verbatim, and nothing else: no stack trace and no InnerException /cause chain today.

Unsupported members show up as build warnings

Every member the reader excludes, and every enum, struct, or interface it can't bind, is recorded with its type, member, and reason, and logged as a Gradle build warning when nugetGenerateBindings runs. If a method you expected in Kotlin is missing, search the build log for it by name rather than guessing why. An unsupported member never hides its siblings: the rest of an overload set, or the rest of a class, still binds. For registration-time failures (a stale build, a contract mismatch at process startup) rather than an extraction-time skip, see Registration diagnostics.

Last modified: 19 September 2026