Hello World [C#]
Introduction
The C# binding talks to the prebuilt libazul native library through P/Invoke, but
you almost never see that layer. You write idiomatic C# — plain classes, the
wrapper-class App.Create(...).Run(wco) path, and a typed layout delegate that
returns a Dom — and the generated Azul.cs handles the marshalling. No
Marshal.AllocHGlobal, no .Raw extraction, no IntPtr ceremony in your code.
Installation
You need .NET 8+ and the native libazul library for your platform.
Recommended: NuGet package
dotnet add package Azul
Note
The 0.2.0 NuGet feed is hosted on azul.rs. If nuget.org does not yet resolve
the package, add the azul.rs source first:
dotnet nuget add source https://azul.rs/nuget/index.json -n azul
If a package is not yet published for your platform, use the manual route below.
Manual
-
Download the native library for your OS from the /releases page and keep it next to your binary (or on the loader path):
# macOS wget -O libazul.dylib https://azul.rs/release/0.2.0/libazul.dylib # linux wget -O libazul.so https://azul.rs/release/0.2.0/libazul.so # windows # download https://azul.rs/release/0.2.0/azul.dll -
Add the generated
Azul.csbindings to your project (ships in the examples archive undercsharp/).
The native library must be discoverable at runtime via DYLD_LIBRARY_PATH (macOS),
LD_LIBRARY_PATH (Linux), or PATH (Windows).
Simple „Counter“ Example
using System;
using Azul;
namespace HelloWorld
{
// Plain C# class - the "single source of truth" for app state.
public sealed class MyDataModel
{
public uint Counter;
public MyDataModel(uint counter) { Counter = counter; }
}
public static class Program
{
private static readonly MyDataModel _model = new MyDataModel(5);
// Click callback: returns an Update as an int. RefanyGet recovers
// your object from the type-erased handle; `as T` is null on mismatch.
private static int OnClick(IntPtr dataPtr, IntPtr infoPtr)
{
var m = HostInvoker.RefanyGet(dataPtr) as MyDataModel;
if (m == null) return (int)AzUpdate.DoNothing;
m.Counter += 1;
return (int)AzUpdate.RefreshDom;
}
// Layout callback: f(data) -> Dom. Runs on startup and again after any
// callback that returns Update.RefreshDom.
private static Dom Layout(IntPtr dataPtr, IntPtr infoPtr)
{
var m = HostInvoker.RefanyGet(dataPtr) as MyDataModel;
if (m == null) return Dom.CreateBody();
var label = Dom.CreateDiv()
.WithCss("font-size: 32px;")
.WithChild(Dom.CreateText(m.Counter.ToString()));
var buttonDom = Button.Create("Increase counter")
.WithButtonType(AzButtonType.Primary)
.OnClick(m, new Func<IntPtr, IntPtr, int>(OnClick))
.Dom();
return Dom.CreateBody()
.WithChild(label)
.WithChild(buttonDom);
}
public static int Main(string[] args)
{
// `using` disposes the App (and calls the C-side delete) on exit.
using var app = App.Create(HostInvoker.RefanyWrap(_model), AppConfig.Create());
app.Run(WindowCreateOptions.Create(new Func<IntPtr, IntPtr, Dom>(Layout)));
return 0;
}
}
}
Four things to notice.
HostInvoker.RefanyWrap/RefanyGet— yourMyDataModelis wrapped into a type-erased handle when you hand it toApp.Create, and the same instance is handed back to every callback.RefanyGet(ptr) as MyDataModelis the runtime cast; it returnsnullon a type mismatch, so returnUpdate.DoNothing/Dom.CreateBody()in that case.- Wrapper-class API, no IntPtr ceremony.
App.Create(...).Run(...),Dom.CreateBody().WithChild(...), andButton.Create(label).WithButtonType(...).OnClick(...).Dom()read like normal fluent C#. TheWithCss("...")builder accepts any CSS string, including:hover { }/@media/@os(...)inline queries. - Callbacks are delegates. A layout callback is
Func<IntPtr, IntPtr, Dom>; a click handler isFunc<IntPtr, IntPtr, int>returning(int)AzUpdate.*. using var appdisposes deterministically —Dispose()calls the C-sidedelete, so native memory is released when theAppgoes out of scope.
Build and run
# macOS
DYLD_LIBRARY_PATH=. dotnet run
# linux
LD_LIBRARY_PATH=. dotnet run
# windows (azul.dll on PATH or in the working dir)
dotnet run
You should see the window pictured on the hello-world landing page. Click the button: the counter increments, the layout callback re-runs, and the new value renders.
Common errors
DllNotFoundException/Unable to load shared library 'azul'— the native library isn't on the loader path. SetDYLD_LIBRARY_PATH/LD_LIBRARY_PATH, or putazul.dllnext to the executable on Windows.- Counter does not advance —
OnClickreturned(int)AzUpdate.DoNothing. Return(int)AzUpdate.RefreshDomafter mutating. RefanyGet(...) as MyDataModelis null — the handle holds a different type, or it is borrowed elsewhere. ReturnDom.CreateBody()/AzUpdate.DoNothing.
Coming Up Next
- Application Architecture — architecting a larger Azul application
- Document Object Model — the Dom tree: node types, hierarchy, and CSS
- Hello World [Java]