Introdução ao kotlin-native-nuget 🪟
Eu transformei tudo isso em um plugin, então você não precisa se deparar com nenhuma dessas paredes. Você escreve Kotlin e do outro lado sai um pacote NuGet com uma API C# adequada: classes, enums, data classes, coroutines, Flow, generics, interfaces, tudo. Sem boilerplate de FFI, sem código manualmente escrito [DllImport], sem “aqui está um cabeçalho C, boa sorte”. Seus amigos no .NET apenas `dotnet add package` e usam como se fosse C# todo o tempo.
Chama-se kotlin-native-nuget, e é um plugin Gradle. Aqui está a proposta:
A proposta 🛗
plugins {
kotlin("multiplatform")
id("io.github.xxfast.kotlin.native.nuget") version "<version>"
}
kotlin {
mingwX64 { binaries { sharedLib { baseName = "mycatlib" } } }
macosArm64 { binaries { sharedLib { baseName = "mycatlib" } } }
}
nuget {
publish {
packageId = "MyCatLib"
version = "1.0.0"
authors = "yourname"
description = "Minha biblioteca Kotlin/Native"
rootPackage = "com.example.cats"
}
}É isso. Adicione o plugin, aponte para seus alvos nativos e execute a tarefa de publicação. Sai um .nupkg.
Por quê? 🤔
É uma boa pergunta. Eu recebi muito (até naquela palestra)
Kotlin Multiplatform já alcança um número absurdo de lugares: JVM, Android, iOS, JS, Wasm e desktop nativo.
Mas há um grande ecossistema que ele nunca realmente conversou: .NET. Há Unity, MAUI, Blazor e nenhuma maneira limpa de entregar uma lógica compartilhada Kotlin aos desenvolvedores C#.
Enquanto isso, a promessa toda do KMP é escrever sua lógica de negócios uma vez. Sua rede, seus modelos, sua validação, seu sofisticado estado máquina. Se você já fez esse trabalho, por que os desenvolvedores C# no prédio devem reescrevê-lo? Eles não deveriam. Eles devem adicionar seu pacote.
Kotlin de entrada, C# idiomatica na saída 🍬
Esta é a parte da qual eu realmente me orgulho. Não apenas conecta. Conecta em C# o que um desenvolvedor C# teria escrito por conta própria.
Aqui está um Kotlin muito comum:
interface Pet { val nome: String; fun falar(): String }
enum class Humor { FELIZ, SONOLIENTO, CHATINHO }
abstract class Animal(override val nome: String) : Pet
class Gato(nome: String, val vidas: Int = 9) : Animal(nome) {
var irmao: Gato? = null
var humor: Humor = Humor.SONOLIENTO
val brinquedos: List<Brinquedo> = listOf(Brinquedo("Rato", "Cinza"))
val aoMiau: () -> String = { "Miau! Meu nome é $nome" }
override fun falar(): String = "Miau!"
}
data class Brinquedo(val nome: String, val cor: String)
class Caixa<T>(val item: T)
fun dono(nome: String): String? = if (nome == "Oreo") "Isuru" else nullE aqui está o que seu consumidor C# recebe. Gerado automaticamente. Sem edições:
using var oreo = new Gato("Oreo", 9);
oreo.Nome; // "Oreo"
oreo.Falar(); // "Miau!"
oreo.Irmao = new Gato("Mylo", 9); // setter de objeto nulo
oreo.Humor = Humor.Feliz; // enums
using var brinquedo = new Brinquedo("Rato", "Cinza");
brinquedo.ToString(); // "Brinquedo(nome=Rato, cor=Cinza)"
brinquedo.Equals(brinquedo.Copia("Bolinha", "Vermelho")); // igualdade de classe de dados + cópia
IReadOnlyList<Brinquedo> brinquedos = oreo.Brinquedos; // coleções
using var caixa = new Caixa<string>("hello"); // genéricos
string? dono = GatoKt.Dono("Oreo"); // retornos nulos
using var aoMiau = oreo.AoMiau;
aoMiau.Invoke(); // lambdas
IPet pet = oreo; // polimorfismo de interface
Animal animal = oreo; // hierarquia de classes abstratasOlhe para isso. PascalCase métodos. IReadOnlyList<Brinquedo>. Tipos de referência nulos com o ? no lugar certo. IDisposable (using var) para que os manipuladores limpe. Humor.Feliz, não alguma constante inteira. Classe de dados Copia() e igualdade de valor. É como se fosse C#, porque para o consumidor, é C#.
E vai além do snippet. Funções suspensas de Kotlin suspend vêm como async/Task, Flow<T> se torna IAsyncEnumerable<T>, cancelamento de corrotinas conecta-se a CancellationToken, e exceções Kotlin aparecem como exceções .NET com o rastreamento da pilha e cadeia de causa intactos. Genéricos, variação, classes seladas, classes inline, funções de extensão: muito do Kotlin atravessa.
Como o salsicha é feita 🌭
Muito brevemente, porque a parte divertida é que você não precisa se preocupar.
Na hora da compilação, o plugin executa um KSP processor sobre sua API Kotlin pública e constrói um modelo intermediário dela (eu chamo de CIR, uma Representação Intermediária C#). Um renderizador transforma esse modelo em um único arquivo Interop.cs. Seu código Kotlin/Native compila para uma biblioteca compartilhada simples por alvo, e tudo é embalado em um pacote NuGet na layout que o .NET espera (`runtimes/{rid}/native/…`).
Gradle Plugin (Kotlin side) NuGet Package C# Consumer
┌─────────────────────────┐ ┌────────────────┐ ┌──────────────┐
│ Compile Kotlin/Native │ │ native libs │ │ Add package │
│ KSP → CIR → Interop.cs │────>│ Interop.cs │────>│ Build │
│ KotlinPoet → Bridges.kt │ └────────────────┘ │ Run │
│ Link shared libraries │ └──────────────┘
│ Package as .nupkg │
└─────────────────────────┘O ponto importante: o Interop.cs é enviado pré-generado dentro do pacote. Não há ferramentas de consumo, nenhum gerador de fonte que eles precisam instalar, nenhuma etapa de construção que eles precisam entender. Eles adicionam o pacote e as associações estão lá. Em tempo de execução, C# chama Kotlin através do ABI C via P/Invoke, mas ninguém precisa olhar para isso.
E vai na direção oposta também ↩️
O mesmo plugin pode consumir um pacote NuGet de Kotlin. Ponto para uma dependência, diga quais namespaces você quer, e ele gera associações idiômaticas de Kotlin para a API C#, da mesma maneira que os plugins CocoaPods e Swift Package Manager trazem pods e pacotes Swift para uma construção Kotlin.
nuget {
dependencies {
dependency("TestDependency", version = "1.0.0") {
bind {
include("Test.Text") // namespaces C# para associar
alias("Test.Text", "sample.text") // namespace C# para pacote Kotlin
}
}
}
}Uma classe C# como esta
public class Template
{
public Template(string template) { ... } // construtor
public string Name { get; set; } // propriedade de instância
public string Apply(string name) { ... } // método de instância
public static Template Parse(string template) { ... } // método estático
public void Use(Action<Template> action) { ... } // padrão IDisposable
}...torna-se Kotlin que você pode chamar:
val template = Template("Hello, {name}") // construtor
template.name = "Oreo" // propriedade de instância
template.apply("Oreo") // método de instância
Template.parse("Hello, {name}") // estático -> objeto companheiro
template.use { Template.render(it, "Oreo") } // manipuladores são AutoCloseableEntão é um verdadeiro cidadão de ambos os ecossistemas de pacotes. Kotlin para NuGet, e NuGet em Kotlin.

