Native Excel add-ins,
written in Zig

Write Zig or Lua functions and use them in Excel. Stream data from any source. Build on Windows, Mac or Linux, deploy to Windows.

View on GitHub

Define a function, get an Excel UDF

Write a normal Zig or Lua function. Declare it with ExcelFunction(). The framework handles everything else: type conversions, UTF-16, error mapping, Excel registration.

  • Zig types convert to XLOPER12 automatically
  • Zig errors become #VALUE! in Excel
  • UTF-8 strings handled natively, framework converts to UTF-16
  • Async support with .is_async = true
  • Return arrays, strings, errors, or scalars
my_functions.zig
const xll = @import("xll");

pub const add = xll.ExcelFunction(.{
    .name = "add",
    .description = "Add two numbers",
    .category = "My Functions",
    .params = &[_]xll.ParamMeta{
        .{ .name = "a", .description = "First number" },
        .{ .name = "b", .description = "Second number" },
    },
    .func = addImpl,
});

fn addImpl(a: f64, b: f64) !f64 {
    return a + b;
}
functions.lua
--- Calculate hypotenuse
-- @param a number Side a
-- @param b number Side b
function hypotenuse(a, b)
    return math.sqrt(a * a + b * b)
end

--- Fibonacci with async support
-- @param n number Index
-- @async
function slow_fib(n)
    local a, b = 0, 1
    for i = 1, n do
        a, b = b, a + b
    end
    return a
end

Write Excel functions in Lua

Not everything needs to be Zig. For many operations, Lua is not meaningfully slower than Zig! Annotate standard Lua functions and the framework generates Excel UDFs from them automatically. Scripts are embedded in the .xll at build time.

The Lua runtime is sandboxed (no filesystem, no shell access) and thread-safe by default. A pool of independent Lua states allows parallel recalculation with no contention.

  • Annotate with @param, @async, @category
  • Automatic type marshalling between Excel and Lua
  • Async Lua functions run on worker threads with result caching
  • Shared state across pool states via xll.get/xll.set
  • Mix Lua and Zig functions in the same add-in

Stream live data into Excel

The NATS connector turns Excel into a reactive computation engine. Subscribe to message streams with a formula. Derive cross-rates, calculate rolling statistics, publish alerts back. All in the spreadsheet.

The NATS.SUBWIN() function provides windowed buffers for time-series analysis directly in cells.

Learn more about the NATS connector →

Live data via RTD
A
B
C
1
Pair
Formula
Value
2
EURUSD
=NATS.SUB("fx.EURUSD")
1.0842
3
GBPUSD
=NATS.SUB("fx.GBPUSD")
1.2631
4
USDJPY
=NATS.SUB("fx.USDJPY")
149.82
5
EURGBP
=B2/B3
0.8584

Build your own RTD server

The NATS connector above is built on zigxll's RTD server framework. You can build your own. Implement a handler; the framework takes care of COM, vtables, registration, and lifecycle.

The counter example in the repo shows how little code it takes. Implement a handler struct and the framework generates the COM server, handles registration, and manages the Excel callback lifecycle.

  • Background thread signals Excel via ctx.notifyExcel()
  • Atomic flag for clean thread shutdown
  • COM registration written to HKCU, no admin rights needed
  • In Excel: =RTD("zigxll.rtd",,"")

View source on GitHub →

timer_rtd.zig
const TimerHandler = struct {
    counter: i32 = 0,
    timer_thread: ?std.Thread = null,
    running: std.atomic.Value(bool) =
        std.atomic.Value(bool).init(false),

    pub fn onStart(self: *TimerHandler,
                   ctx: *rtd.RtdContext) void {
        self.running.store(true, .release);
        self.timer_thread = std.Thread.spawn(
            .{}, timerProc, .{ self, ctx },
        ) catch return;
    }

    pub fn onRefreshValue(self: *TimerHandler,
        _: *rtd.RtdContext, _: i32) rtd.RtdValue {
        return .{ .int = self.counter };
    }

    fn timerProc(self: *TimerHandler,
                 ctx: *rtd.RtdContext) void {
        while (self.running.load(.acquire)) {
            std.Thread.sleep(2 * std.time.ns_per_s);
            self.counter += 1;
            ctx.markAllDirty();
            ctx.notifyExcel();
        }
    }
};
async_example.zig
pub const slow_calc = xll.ExcelFunction(.{
    .name = "SlowCalc",
    .description = "Heavy computation",
    .is_async = true,
    .params = &[_]xll.ParamMeta{
        .{ .name = "n" },
    },
    .func = slowCalcImpl,
});

fn slowCalcImpl(n: f64) !f64 {
    // Runs on a background thread.
    // Cell shows #N/A, then updates
    // when complete.
    return expensive_work(n);
}

Async functions

Add .is_async = true to any function definition. This make it run on a background thread pool. The cell shows #N/A while computing, then updates with the result automatically.

Results are cached, keyed by function arguments, so subsequent recalculations return instantly. Works for both Zig and Lua functions. Useful for slow computations, network calls, or anything you don't want blocking Excel's main thread.

Get started in minutes

1

Create a new project from the template

Use the GitHub template to scaffold a new repo with everything wired up, or add zigxll as a dependency in an existing project's build.zig.zon:

.xll = .{ .url = "https://github.com/alexjreid/zigxll/archive/refs/tags/v0.3.1.tar.gz", .hash = "..." }
2

Define your functions

Write Zig functions, declare them with ExcelFunction(). List your modules in main.zig:

pub const function_modules = .{ @import("my_functions.zig") };
3

Build

Cross-compiles to a Windows .xll from any platform. Tests run natively.

zig build # produces zig-out/lib/my_functions.xll
zig build test # runs tests natively, no Windows SDK needed
4

Load

Double-click zig-out/lib/my_functions.xll to load in Excel.

Why Zig for Excel add-ins?

XLL add-ins are native DLLs running inside the Excel process. The C SDK is from the early 1990s. A hostile beast, zigxll makes it approachable.

Direct C interop

Zig consumes C headers directly. No bindings or code generation step. Minimal overhead at runtime.

Comptime code generation

All the boilerplate (exports, type conversions, registration, COM vtables) is generated at compile time from your function definitions. This means high performance at runtime.

📦

Small, self-contained binaries

A single .xll file, a few hundred KB.

🌐

Cross-compile from anywhere

Build Windows .xll files from macOS or Linux. No Windows SDK, no Visual Studio. Tests run natively on any platform.

🧵

Multi-threaded by default

Excel can parallelise function calls across cores during recalculation. ZigXLL functions are thread-safe by default (MTR).

🛡

Explicit memory control

Arena allocators and explicit lifetime management. No GC pauses, no hidden allocations, predictable performance.

Learn more

Blog post

Building Excel XLL Add-ins in Zig

The motivation behind zigxll. Why Zig, how comptime eliminates boilerplate, and real-world implementation details.

Blog post

Excel: The Accidental Stream Processor

Using zigxll's NATS connector to turn Excel into a reactive stream processor for FX monitoring.

Documentation

Functions, RTD, Lua, Architecture

Full docs covering function definitions, real-time data servers, embedded Lua scripting, and how it all works.

Example

Complete Working Project

A full example project you can clone, build, and load into Excel.

Template

Standalone Template Repo

Start a new project with the template. Everything set up and ready to go.

Connector

NATS Pub/Sub for Excel

Stream NATS messages into Excel as live data. Subscribe, derive, publish back. ~100KB binary.

Build your first XLL

Clone the template, write your functions, run zig build, load in Excel.

Use the template Read the source