Instance members
Instance methods and instance properties on a bound C# class become member functions and properties on the generated Kotlin wrapper (see Objects and handles). Every call passes the wrapper's handle as the receiver, the same mechanism a handle-typed parameter already uses elsewhere.
A read-only property ({ get; }, or a getter-only expression body) becomes a Kotlin val. A settable property ({ get; set; }) becomes a var. Neither is a stored field: every access calls through the bridge, just like a method call.
An init-only property ({ get; init; }) also becomes a val, not a var: Kotlin has no object initializer to assign it through, so there is no setter to bind and no way to write it after construction from Kotlin.
Overloads
Same-name C# instance methods become ordinary Kotlin overloads, resolved by parameter type:
An overload set the mapping can't tell apart is skipped with its own build diagnostic; the type's other bridgeable members still generate.
Choosing the C# name
@CSharpName sets the exact C# name of one fun or property. Use it when a property and a function share a Kotlin name, which C# cannot declare on one type (ERROR_CSHARP_NAME_COLLISION), and you don't want to rename the Kotlin API on every platform:
The name is used verbatim. A
suspend fungets noAsyncsuffix unless you write it.The name must be a C# identifier, or generation fails with
ERROR_CSHARP_NAME_INVALID. A C# keyword is allowed and is escaped with@on every member, so@CSharpName("event")is called asx.@event.A Kotlin name written in backticks needs no annotation when it is a keyword:
fun `in`(`object`: Int)binds asIn(int @object), on every route, including a listener interface and aResultTrytwin (an authoredfailureparameter becomesfailure_;valuekeeps its name). A name with a space or symbol (fun `tug hard`()) has no C# spelling, so it is skipped with aSKIPPED_UNSUPPORTED_COMBINATIONwarning (a property:SKIPPED_UNSUPPORTED_PROPERTY) that names it.@CSharpName("TugHard")makes it bind; the native entry point replaces each run of other characters with_(tug_hard). Two names that clean to the same entry point, such as`tug hard`andtug_hard, fail withERROR_C_ENTRY_POINT_COLLISION. Kotlin/Native itself cannot link a library whose public member is named with a space, so rename such a member in Kotlin rather than rely on the annotation. A listener interface with a space-named member and no@CSharpNamegets no bridge factory, and itsaddX/removeXpair is skipped naming that member; name the member or rename it.It also applies to
FlowandStateFlowproperties and methods, and to theaddXhalf of an add/remove listener pair (the member you call). On theremoveXhalf it does nothing, with no warning: that half names no C# member.An override inherits the name declared on the member it overrides. Repeating the same name is fine; a different one fails with
ERROR_CSHARP_NAME_OVERRIDE_MISMATCH, so put the annotation on the root declaration.If the declared name still collides with another member, you get
ERROR_CSHARP_NAME_COLLISIONagain, with the annotation shown in the message.
Only the C# member changes. The Kotlin name and the native entry points stay as they were. The plugin adds the annotation's nuget-annotations dependency to commonMainApi for you (see the plugin DSL). A dependency module that doesn't apply the plugin applies the annotations plugin to annotate its own members; see the same section.
Name collisions with the wrapper itself
The generated wrapper already owns three Kotlin member names: handle, close, and cleaner (see Objects and handles). A C# instance method or property whose camelCased Kotlin name would collide with one of those is skipped, with a Gradle build warning naming the member and asking you to rename it on the C# side or expose it through a differently-named adapter member. Static members are unaffected: they land in the Kotlin companion object, a separate name scope from the wrapper's own instance members (see Static classes and methods).
Handle-typed properties
A settable property whose type is itself a bound class renders as a Kotlin var, and its nullability follows the property's NullableAttribute, exactly as for a handle-typed method parameter or return (see Objects and handles):
Because a property carries exactly one nullability annotation, its Kotlin getter and setter always agree on the same type, so a settable handle-typed property is never forced to a read-only val the way a mismatched getter/setter pair would be.
Async methods
A Task - or Task<T>-returning method, instance or static, becomes a Kotlin suspend fun returning Unit or T:
A trailing Async is dropped, unless the declaring type also has a method whose C# name equals the stripped name, in which case the Async-suffixed name is kept so the two don't collide (Kotlin can't overload on suspend alone):
A faulted task surfaces as a catchable NugetManagedException carrying the original .NET exception type, never AggregateException:
The completion runs on a .NET thread-pool thread, never inline on the thread that started the call. A task that ends cancelled is different from an ordinary fault: see Cancellation below.
Cancellation
An async method with exactly one CancellationToken parameter, at any position, binds with the token elided; the bridge supplies it, so cancelling the Kotlin side that's awaiting the call actually tells the C# work to stop:
This covers withTimeout/withTimeoutOrNull, job.cancel(), and disposing an enclosing coroutineScope, the same as any other coroutine cancellation. A = default value on the parameter makes no difference; the bridge always supplies its own token, never the default. A method that ignores its token behaves as before: the Kotlin wait still ends promptly, but the C# work runs to completion regardless.
A C# task that ends cancelled (OperationCanceledException or any subtype, including TaskCanceledException, thrown from a sync or async call) surfaces as stdlib kotlin.coroutines.cancellation.CancellationException instead of NugetManagedException, with the original NugetManagedException riding as cause:
This is the one case worth being careful with: a CancellationException thrown into a coroutine that Kotlin itself never cancelled ends that coroutine silently, without failing its parent, the same footgun a manually-thrown CancellationException is anywhere in kotlinx. Catch it explicitly, as above, around any call whose C# side might cancel itself.
Two C# overloads that differ only by a trailing CancellationToken (FooAsync() and FooAsync(CancellationToken)) both elide to the same suspend fun foo(); the reader keeps the token-taking overload and drops the other, so a consumer only ever sees one foo().
A sync method taking a token, a method taking two or more tokens, a nullable CancellationToken?, or a token on a constructor or property, is not bound; it's a named diagnostic (INFO_CANCELLATION_TOKEN_NOT_YET_MAPPED) rather than the generic SKIPPED_UNBOUND_TYPE_REFERENCE hint.
Async streams
An IAsyncEnumerable<T>-returning method, instance or static, becomes a Kotlin Flow<T>:
The naming and cancellation-token elision rules are the same as async methods and cancellation above; a trailing Async drops the same way, and a single CancellationToken parameter elides.
The flow is cold, and the C# method itself runs at collect, not at the Kotlin call that returns the Flow. One Flow value collected twice runs the C# method twice:
A synchronous argument-validation throw from the C# method therefore surfaces at collect, not where you called kennel.barks(...), and a method with side effects runs once per collect rather than once per Flow value.
Collector cancellation (withTimeoutOrNull, take(1), a cancelled parent job) stops the C# enumeration, but only promptly when the source itself marks its token parameter [EnumeratorCancellation] (LitterAsync above); without it, the current step finishes and yields one more element before C# notices. Either way, the collector receives nothing past the point it cancelled: no element is ever delivered after cancellation. Disposing the C# enumerator is fire-and-forget: collect can return before the C# iterator's own finally has run, and an exception thrown from that finally is silently dropped. A mid-stream throw from the C# source surfaces the same way a faulted Task does, as a catchable NugetManagedException:
IAsyncEnumerable<T> at a parameter, property, constructor, or type-argument position, IAsyncEnumerable<T>?, a nullable value element (IAsyncEnumerable<int?>), and async on a bound interface, a struct, or a generic class, are not bound; see Limitations below.
Limitations
Nullable<T>value-typed instance properties and parameters (int?,CatMood?) are not yet supported.Struct-typed instance properties and methods are supported; see C# structs.
ValueTask/ValueTask<T>methods, and an async method (includingIAsyncEnumerable<T>) on a bound interface, a struct, or a generic class, don't bind yet; see The bridgeable subset.IAsyncEnumerable<T>at a parameter, property, constructor, or type-argument position,IAsyncEnumerable<T>?, and a nullable value element (IAsyncEnumerable<int?>), don't bind yet.A sync method taking a
CancellationToken, a method taking more than one, a nullableCancellationToken?, and a token on a constructor or property are not yet bound.