Extensions
An extension function renders as a genuine C# extension method (this parameter). An extension property renders as a C# 14 extension property, so it reads (and, for a var, writes) like a property. Both are grouped by receiver into a {Receiver}Extensions static class, alongside any other top-level declarations (see Top-level declarations).
From test-library/src/nativeMain/kotlin/.../cat/CatExtensions.kt and StringExtensions.kt:
An extension property becomes a C# 14 extension block, so it reads and writes like a property. The generated package therefore requires C# 14 (net10.0).
Extension properties as C# 14 extension blocks
Two imported namespaces can each declare a property with the same name on the same receiver, which makes cat.IsKitten ambiguous. Call the lowered accessors on the declaring class instead: CatExtensions.get_IsKitten(cat) and CatExtensions.set_Label(cat, value).
Where the generated class lands
An extension on an exported receiver (a class this library publishes, e.g. Cat) lands in that receiver's own namespace (TestLibrary.Cat.CatExtensions). An extension on an unexported receiver (String, a primitive, any stdlib type) has no such home, so it lands in the namespace of the package that declares the extension instead. Extensions on the same unexported receiver declared in the same package share one class; a different package gets its own class in its own namespace, and never merges with the first.
Extension-method call syntax ("Oreo".Meowify()) compiles wherever the receiver's extensions land, as long as the calling file has that namespace in scope via using. Only a fully-qualified static call pins the namespace at compile time:
Supported receivers
An extension, function or property, sync or suspend, binds on every receiver the bridge can pass as a parameter, nullable or not, and the receiver crosses on the same wire that parameter would.
Kotlin receiver | C# receiver |
|---|---|
Exported class, nested class, eligible sealed base |
|
Exported generic class |
|
Bare type parameter |
|
Star projection ( |
|
Plain interface, bound C# interface |
|
|
|
Value class over any underlying |
|
Enum |
|
|
|
|
|
|
|
The nullable form of any of these |
|
A receiver that is an unexported, non-stdlib type has no C# type to extend and is dropped with a named diagnostic: SKIPPED_UNSUPPORTED_INPUT for a function, SKIPPED_UNSUPPORTED_PROPERTY for a property (see Publishing Kotlin to C#: Diagnostics). An extension property typed Flow, StateFlow, or a lambda is skipped the same way, even on an otherwise-supported receiver.
Generic and bare T receivers
An extension on a generic class, or on a bare T, binds as a generic C# extension method, and an extension property binds as a C# 14 extension<T>(...) block. T is inferred from the receiver.
A bare T needs a class to live in. A bounded one (T : Dozer) goes on the bound's own extension class, DozerExtensions, in the bound's namespace, with where T : IDozer. With several bounds (where T : Dozer, T : Preener) it goes on the first declared bound. An unbounded T (including T : Any) goes on ObjectExtensions in the namespace of the package that declares the extension. It is visible on every C# type wherever that namespace is imported, which is what a Kotlin fun <T> T.tag() does too.
A receiver that nests T in a shape the bridge cannot carry as a parameter is a named GENERIC skip.
Projection receivers
C# has no projection of a generic class, so a receiver whose type arguments are all * gets a generic method with a type parameter the bridge names after the class (TBindle, TList). Several stars are numbered (TMap1, TMap2).
This works for a function, a property and a suspend function. A projection is still not supported for a generic class as a parameter, a result or a property type (a star-projected generic value class is the exception, see A star-projected result). A receiver with only some arguments starred (Map<String, *>) is not covered.
Member extensions
An extension declared inside a class binds as an instance method of that class, with the receiver as its first parameter named receiver. C# cannot declare an extension method in a non-static class, so loafer.Brush(...) is not available. Kotlin/JVM lowers it the same way. A member extension property becomes accessor methods, GetX(receiver) and, for a var, SetX(receiver, value).
A nullable receiver keeps its ? (GetNickOrNone(Loafer? receiver)).
On every owner
A member extension declared in any other owner binds too, and the receiver stays an argument:
Owner | Property | Function |
|---|---|---|
| static | static |
enum |
|
|
value class |
|
|
sealed base and arm |
| as on a class |
interface |
|
|
A Flow typed property is a flow method on each (Napkin.Snores(loafer)). On the enum and value class suspend and Flow methods the extension receiver is named receiver_, because receiver is already the owner; calls are positional, so only IntelliSense shows it.
A Kotlin implementer of IChorus works, and so does a C# one, the receiver its first argument, whether it is a String or a class (string GetHideout(Loafer receiver) on IHideaway).
suspend and Flow extensions
A suspend extension is a ...Async extension method on the same receivers. A suspend function returning Result<T> binds as Task<T> and a failure faults the task (see Coroutines and Flow).
A sync extension returning Flow, StateFlow or SharedFlow returns the matching wrapper, on a class, generic, nullable, collection or nullable enum receiver. A Flow<T> parameter binds on a generic and a nullable receiver too.
An extension property is also skipped, named SHADOWED_BY_MEMBER, when the receiver already has a visible member property of the same name (declared or inherited):
Kotlin resolves receiver.x to the member in both the generated export body and in your own call sites, so the extension is unreachable by plain call syntax, not merely un-exported. Rename the extension property or expose a top-level function instead. A private or protected member does not shadow, and neither does a nullable receiver (val Foo?.x).
An extension function shadowed by a member is kept, unlike a property. FooExtensions.Y(foo) runs the extension and foo.Y() runs the member, as Kotlin and C# each resolve the call. Declare the extension in the member's own package or in another one; both bind the same way. The exception is an enum receiver: an enum member and a same-package extension of the same name fail with ERROR_CSHARP_SIGNATURE_COLLISION, because both land in the enum's Extensions class. Rename one.
An extension property is skipped, named SHADOWED_BY_EXTENSION_FUNCTION under SKIPPED_UNSUPPORTED_PROPERTY, when an extension function on the same receiver has the same C# name, because cat.NameOrStray would be ambiguous. The function keeps the name. Put @CSharpName on the property to keep both:
The same skip applies when the receiver's own enum, class or value class declares a member function with that C# name, at any arity (fun grooming(times: Int) beside val Cat.grooming), because a member method makes cat.Grooming unreachable as a property. The property is skipped with the warning and the member function keeps the name; @CSharpName on the property keeps both. Before this rule an enum member function without parameters was a build error and the other cases shipped C# that failed to compile.
The clash is per generated C# extension class. On an exported receiver such as Cat, extensions from every package merge into one CatExtensions, so the skip applies across packages. On an unexported receiver such as String, each package gets its own StringExtensions, so a property and a function of the same name in different packages both bind. A file that imports both namespaces gets CS9339 on the property syntax; call A.StringExtensions.get_Tag(s) instead.
Two extension properties with the same C# name on the same receiver are a build error (ERROR_CSHARP_SIGNATURE_COLLISION); rename one with @CSharpName.
val Cat.x beside val Cat?.x compiles in Kotlin but is also a build error with ERROR_CSHARP_SIGNATURE_COLLISION, because C# reads Cat and Cat? as one receiver type and cannot declare both. @CSharpName does not separate them; rename one of them in Kotlin. A value-type receiver is different: int and int? are two receivers to C#, so val Int.x beside val Int?.x binds both. The same goes for Char, an enum, Instant, Duration, Uuid and a value class. A property and a function of one name also bind when their value-type receivers differ in nullability (fun Int.label() beside val Int?.label); the same nullability is still SHADOWED_BY_EXTENSION_FUNCTION.
Instant, Duration, and Uuid map to DateTimeOffset, TimeSpan, and Guid at a receiver the same way they do everywhere else (see Primitives and strings). A bound C# interface (see The bridgeable subset) is a receiver too, nullable included.
Nullable receivers
An extension function on a nullable receiver (Cat?) is still a genuine C# extension method, so calling it on a null reference is legal and reaches Kotlin: extension methods dispatch statically, there is no NullReferenceException.
An extension property on a nullable receiver (val Cat?.x) behaves the same way.
A nullable value-type receiver (Int?, Char?, an enum, Instant?, Duration?, or a value class over a primitive or enum) binds as an extension method on the matching Nullable<T>, and a C# null reaches Kotlin as a real null. An Int and an Int? receiver of the same name become two overloads and each call picks its own:
As with a nullable value class, the receiver is nullable only: calling OrNoLives on a bare int fails to compile (CS1929), so call it on an int? variable.
Extension properties on these receivers bind the same way, as C# 14 extension(int? receiver) blocks, and a var gets its setter:
A property declared only on Int? is read on an int? variable; a bare int is CS1929. A setter needs a variable on the left (none.LivesNote = "x"), since C# rejects an assignment through an rvalue.
Interface receivers
An interface receiver works whether the concrete instance is Kotlin-backed or implemented in C#, the same as an interface parameter: the plugin dispatches back to whichever side actually implements the members, so a C# Dog : IPet behaves correctly without anything extra on the caller's part.
On an extension property
An extension property's receiver may be a bare interface too, with the same dispatch behavior:
Converting and collection receivers
An extension property's receiver may also be a converting scalar (Enum, Uuid, Instant, Duration), a Collection, or a bound C# interface from a NuGet dependency, the same shapes an extension function receiver already takes. A var works too:
A collection receiver (val List<String>.longestName) disposes a handle the C# side builds for the crossing, same as any other collection argument; a bound-interface receiver (val IFeedable.feedingNote) dispatches back into the C# object the same way an interface receiver does.
Sealed receivers
An extension function's or property's receiver may be an eligible sealed base (see Sealed interfaces). The extension binds on the abstract base and every arm inherits it, so you don't need to when-match the arm just to call it:
Value-class receivers
A value class works as a receiver over any supported underlying (see Value classes):
A String - or object-handle-underlying value class receiver may also be nullable:
CatId generates as a readonly record struct, so the receiver is CatId? (Nullable<CatId>). C# only binds an extension receiver through an identity, implicit-reference, or boxing conversion, and CatId -> CatId? is none of those, so calling the extension on a bare CatId fails to compile (CS1929):
Guid? has the same asymmetry for the same reason: a nullable Uuid receiver (val Uuid?.isMissing) only binds this Guid? receiver, so missing.IsMissing needs a Guid? local, not a bare Guid.
A value class whose underlying can be null (value class Nick(val name: String?)), and a generic one over an unconstrained T (Sticker<T>), may be a nullable receiver too. null and Nick(null) stay different values, and the generic receiver binds as a generic extension method:
A top-level extension with its own type parameter (fun <T> X.f()) binds as a generic extension method like any other top-level fun <T>. A Sticker<T>? receiver needs the explicit type argument when you pass null, as in the last line.
Nested receivers
A nested class works as a receiver too, binding under its own owner chain the same way a member of that class does (see Nested types). The generated class stays at namespace level rather than nesting (AviaryPerchExtensions, not Aviary.PerchExtensions), since C# forbids nesting an extension class; extension-call syntax is unaffected:
The same expansion applies when the receiver is spelled through a typealias of a nested type (typealias Bird = Aviary.Bird; fun Bird.sing()): the generated class is AviaryBirdExtensions, not BirdExtensions, so it never collides with an unrelated top-level Bird's own extensions. This only changes the internal entry point the generated Sing method calls into, not the C# surface; since the C# shim and native library always ship together in one package, upgrading the plugin changes nothing a consumer needs to do.
Overloads and default parameters
Two or more same-named extensions on the same receiver render as one ordinary C# overload set, resolved the normal C# way by parameter types:
A defaulted extension parameter widens the same way a class method or top-level function's does (null means "use the Kotlin default"). The receiver is never a parameter the widening rule considers, so it is always required:
A defaulted parameter the bridge cannot route to any non-null C# form is dropped from the C# signature when it is trailing, and Kotlin always evaluates its default.
The events parameter does not exist in C# at all. See Constructor and method default parameters.
Return values
An extension function's return goes through the same marshalling as a class method's (see Classes and objects): converted objects, nullable objects, collections, and nullable primitives all follow the same rules. A returned object still needs disposal the same way any other returned handle does.