Why I started
Okay okay, I know that this sounds extremely ambitious. But! Let me first introduce myself.
I'm Nikola, I've been in the development space for almost 10 years professionally now , and my first couple of years I've been mostly working as a professional Unity Developer, and that's where I fell in love with both cross platform and C#. After couple of years doing that professionally I started working on Unity SDK, and there I found how to actually write native code that can easily communicate with the actual platform you're building for, and that was an interesting moment for me. Since then I've transitioned to be more mobile-focused and have fell in love with Flutter (and occasionally have to use React and React Native in my day-to-day job), and the way it's done communication with native platforms was kinda cool.
For the past year, I'm working mostly on full mobile SDKs in blockchain space, from Kotlin and Swift to Flutter and occasional React Native. Then I wanted out of curiosity to create a .NET MAUI SDK just to see how things would work there, and my God I hated every step of the way there. I hate XAML MVVM and all of the stuff .NET Framework had put as a standard. I do understand that some people love it, but coming from Flutter Swift and Kotlin where things are done differently, I couldn't get back to the 'enterprise' way of writing things. And then comes the 'talking with native platforms' part which made me sick to my stomach. I couldn't write native files, but had to compile libraries and copy them over and then and ONLY THEN I can read things from them, that feels like a big big step back in the way I'm doing my development (even tho almost all of it is agentic nowadays, but still!).
My main motivation behind DotNative (working title) is to make .NET appealing to people either wanting to try .NET or for the companies to have a full stack in backend being .NET and frontend also being .NET.
So let me simplify the architecture of the Framework and of course the inevitable plugin system.
Writing an app
The basic idea is pretty simple: you write your application in C#, Rust handles the UI tree and layout, and the operating system draws the actual controls.
So yes, C# and Rust in the same mobile framework. Apparently I decided one language wasn’t enough trouble 😄
But as an app developer, you shouldn’t have to care about the Rust part. You write components, define state, register your services, and build your app.
Here’s what that currently looks like:
using DotNative;
public sealed class App : Component
{
private readonly State<int> _count = new(0);
public override Element Build() =>
new VStack(
new Text("Hello, DotNative!")
.FontSize(32),
new Text($"You clicked {_count.Value} times")
.FontSize(20),
new Button("Click me", () => _count.Value++)
.Padding(16)
.BackgroundColor(Color.Blue)
.TextColor(Color.White)
.CornerRadius(12)
)
.Padding(24)
.Spacing(16);
}That’s a label, some reactive state, and a button. Change the state and the framework schedules a rebuild, compares the resulting tree with the previous one, and sends the necessary updates to the native side. Hot Reload is of course supported as well.
Styling is also just C#. Extension methods and component composition. If I want a reusable primary button, I can build a component that applies those styles once and use it throughout the app.
And the application starts from an actual C# Main:
using DotNative;
public static class Program
{
public static void Main()
{
var builder = DotNativeApplication.CreateBuilder();
// Register your services here.
builder.Build().Run<App>();
}
}You get ordinary .NET dependency injection here. Shared application state can live in services, screens can receive dependencies through their constructors, and you can organize your application using the .NET tools you already know.
Where Rust comes in
Now, where does Rust come in?
C# builds the description of the UI, but it doesn’t call into native code separately for every property on every control. Instead, we collect the changes into a binary command buffer.
Think commands like “create this node,” “update this text,” “apply these styles,” and “attach these children,” encoded as opcodes and their payloads.
We’re sending bytes, not JSON. There’s no runtime JSON serialization or CSS parser involved. C# passes the buffer through a small C ABI, and Rust reads it synchronously. The native side borrows that buffer during the call; anything it needs afterward becomes owned native data.
Rust validates those commands, maintains the UI tree, and calculates layout using Taffy, a Rust layout library with Flexbox support. The platform layer then applies that layout to real native views.
On iOS, that means UIKit controls. On Android, Android views. On macOS, AppKit.
The OS does the painting. Rust coordinates the tree, layout, and platform updates. Button presses and other native input events travel back to C#, where the application callbacks run on its UI dispatcher.
I’m deliberately keeping the performance claims modest for now. Batching commands is useful, but it doesn’t magically make the entire framework “zero overhead.” We still have allocations, reconciliation, layout, and native work to measure. Right now, this is a working prototype, and I want actual benchmarks before making ridiculous promises.
Native plugins
The plugin system is the other big part of this.
Because honestly, if I can build a beautiful counter but can’t comfortably access the camera, pick a file, or integrate an existing native SDK, what have I really built?
The experience I want is a plugin package containing its C# API alongside actual Swift and Kotlin source files. The build tooling takes care of compiling and including those native files. Plugin authors shouldn’t have to manually produce an archive and drag it into every consuming app’s Xcode project whenever they change something.
Talking to the platform
For communication, I've started with a Flutter-inspired channel system.
A plugin declares its channel identity once, and we generate matching identifiers for C#, Swift, and Kotlin. The native implementation registers handlers on that channel. Underneath, requests and responses use a separate binary protocol with call IDs, results, errors, and cancellation.
For example, inside the Kotlin FilePicker implementation, registration looks like this:
val channel = NativeChannels.channel(FilePickerChannel)
channel.handle("pick") { args, reply ->
pick(args, reply)
}FilePickerChannel is generated. You don’t repeat a handwritten channel name across three languages and hope nobody makes a typo.
Method names are still strings inside the plugin for this first version. Fully typed, generated contracts can come later without replacing the transport underneath.
Using a plugin
But that’s plugin-author territory. Someone using the plugin gets a normal C# API.
After adding DotNative.FilePicker, you register it:
builder.Services.AddFilePicker();Then a service can receive the interface supplied by that package:
using DotNative.FilePicker;
public sealed class RandomService(IFilePicker picker)
{
public async Task<byte[]?> ImportAsync(
CancellationToken cancellationToken = default)
{
await using var file =
await picker.PickAsync(cancellationToken);
if (file is null)
return null; // The user dismissed the picker.
await using var input =
await file.OpenReadAsync(cancellationToken);
using var output = new MemoryStream();
await input.CopyToAsync(output, cancellationToken);
return output.ToArray();
}
}On iOS, the plugin opens the system document picker. On Android, it opens the system document picker there. Native file access stays on the device, and C# receives an asynchronous stream API.
If the user dismisses the dialog, you get null. If access fails, you get a managed plugin error you can handle. Denying access should never mean “well, I guess we’re crashing the app now.”
What comes next
And this channel approach also works with our current hot reload setup: C# runs in a development host on the Mac, while the simulator or connected device displays native controls and executes native plugin operations. Release builds use NativeAOT.
There’s still plenty to build. The FilePicker implementation currently targets iOS and Android, native source changes still require a rebuild, and the development CLI’s package discovery needs more work.
But the foundation is there: declarative C#, proper .NET DI, real native controls, and plugins whose native code can actually live with the plugin.
That’s the development experience I’m trying to build. Something I would personally enjoy opening on a Monday morning.
Of course, I am going to open source everything soon, just need to make sure
1. I am behind every single line written in here. Of course I got massive help from the agents, but I don't want to not know my own framework by heart.
2. Still polishing some rough edges, and making samples and plugins that can be useful to the community as soon as the project goes open source.
I am open to questions.