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 the .NET 8 SDK or newer and the native libazul library for your platform. (The example project examples/csharp/Hello.csproj and the downloadable Azul.csproj both target net8.0, which .NET 9 and 10 projects consume as well.)

azul isn't on nuget.org, but a self-hosted NuGet v3 feed lives at azul.rs and the package bundles the native libazul for Linux/macOS/Windows under runtimes/<rid>/native (.NET picks the right RID at runtime). The package id is Azul.Net (the assembly and namespace are still Azul):

dotnet nuget add source https://azul.rs/ui/nuget/index.json --name azul
dotnet add package Azul.Net --version 0.2.0

Or install the native library + binding by hand:

  1. Download the native library for your OS from the release page and keep it next to your binary (or on the loader path):

    # macOS (Apple Silicon; Intel: libazul.x86_64.dylib)
    wget -O libazul.dylib https://azul.rs/ui/release/0.2.0/libazul.dylib
    # linux
    wget -O libazul.so    https://azul.rs/ui/release/0.2.0/libazul.so
    # windows
    # download https://azul.rs/ui/release/0.2.0/azul.dll
    
  2. Add the generated Azul.cs bindings to your project:

    # Azul.cs ships with the GitHub release, not the site: it is far too
    # large for the Pages artifact budget.
    curl -L -O https://github.com/fschutt/azul/releases/download/0.2.0/Azul.cs
    # optional project scaffold:
    wget https://azul.rs/ui/release/0.2.0/Azul.csproj
    

That's it for library discovery: the generated Azul.cs installs a DllImportResolver that probes the app's base directory and the current working directory for libazul.dylib / libazul.so / azul.dll, so keeping the native library next to your project (step 1) is sufficient — no DYLD_LIBRARY_PATH / LD_LIBRARY_PATH setup needed for dotnet run.

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)Update.DoNothing;
            m.Counter += 1;
            return (int)Update.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.CreateTextDoNotUseWithoutBlockLevelWrapper(m.Counter.ToString()));

            var buttonDom = Button.Create("Increase counter")
                .WithButtonType(ButtonType.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 — your MyDataModel is wrapped into a type-erased handle when you hand it to App.Create, and the same instance is handed back to every callback. RefanyGet(ptr) as MyDataModel is the runtime cast; it returns null on a type mismatch, so return Update.DoNothing / Dom.CreateBody() in that case.
  • Wrapper-class API, no IntPtr ceremony. App.Create(...).Run(...), Dom.CreateBody().WithChild(...), and Button.Create(label).WithButtonType(...).OnClick(...).Dom() read like normal fluent C#. The WithCss("...") 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 is Func<IntPtr, IntPtr, int> returning (int)Update.*.
  • using var app disposes deterministically — Dispose() calls the C-side delete, so native memory is released when the App goes out of scope.

Build and run

# all platforms — the native library sits next to the project (step 1),
# and the generated DllImportResolver finds it there
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 wasn't found. Put libazul.dylib / libazul.so / azul.dll in the project directory (or next to the published executable); the generated resolver probes both. DYLD_LIBRARY_PATH / LD_LIBRARY_PATH also work as a fallback for non-standard layouts, but note macOS strips DYLD_* in some launch paths (SIP).
  • Counter does not advance — OnClick returned (int)Update.DoNothing. Return (int)Update.RefreshDom after mutating.
  • RefanyGet(...) as MyDataModel is null — the handle holds a different type, or it is borrowed elsewhere. Return Dom.CreateBody() / Update.DoNothing.