Darp.Luau is a .NET wrapper around Luau focused on native AOT compatibility, typed value access, and explicit ownership for Luau-backed references.
- NativeAOT first
- Typed reads and writes for tables, functions, userdata, strings, and buffers
- Clear lifetime guarantees both stability and performance
- Simple API through source-generated interceptors
- Host modules and managed userdata
- Support for
linux,windows,macoson bothx64,arm64
using Darp.Luau;
using var lua = new LuauState();
using LuauFunction log = lua.CreateFunction((string message) => Console.WriteLine(message));
lua.Globals.Set("log", log);
using LuauTable config = lua.CreateTable();
config.Set("name", "Ada");
config.Set("enabled", true);
lua.Globals.Set("config", config);
lua.Load(
"""
function add(a, b)
return a + b
end
log(config.name)
result = add(20, 22)
"""
).Execute();
double result = lua.Globals.GetNumber("result");using LuauFunction add = lua.Globals.GetLuauFunction("add");
double sum = add.Invoke<double>(1, 2);
using LuauFunction pair = lua.Globals.GetLuauFunction("pair");
(int total, int delta) = pair.Invoke<int, int>(20, 4);Invoke<TR>(...) converts a single Luau return value to the managed type you ask for and ignores extras. Use Invoke<TR1, TR2>(...), ... for typed multi-return calls, and InvokeMulti(...) for raw LuauValue[] access. The current argument buffer accepts up to 4 arguments per call.
Load(...).Execute(...) follows the same return-shaping pattern for chunk execution: use Load(...).Execute<TR>() for the first typed return value, Load(...).Execute<TR1, TR2>(), ... for typed multi-return calls, and Load(...).ExecuteMulti() for raw LuauValue[] access.
(int total, int delta) = lua.Load("return 20, 4").Execute<int, int>();Use CreateFunction(...) for supported fixed delegate signatures:
using LuauFunction sum = lua.CreateFunction((int a, int b) => a + b);
lua.Globals.Set("sum", sum);
using LuauFunction pair = lua.CreateFunction((int a, int b) => (a + b, a - b));
lua.Globals.Set("pair", pair);Use CreateFunctionBuilder(...) when you need manual argument parsing, explicit user-facing errors, or a callback shape that the generator-backed path does not support:
using LuauFunction pair = lua.CreateFunctionBuilder(static args =>
{
if (!args.TryValidateArgumentCount(2, out string? error))
return LuauReturn.Error(error);
if (!args.TryReadNumber(1, out int a, out error) || !args.TryReadNumber(2, out int b, out error))
return LuauReturn.Error(error);
if (a <= b)
return LuauReturn.Error("Expected a to be greater than b");
return LuauReturn.Ok(a + b, a - b);
});CreateFunction(...) must be called directly at the call site so the generator can intercept it. It supports fixed delegate signatures, including supported top-level tuple returns. If you need a shape that is not supported there, use CreateFunctionBuilder(...).
using LuauTable settings = lua.CreateTable();
settings.Set("volume", 0.8);
settings.Set("muted", false);
settings.Set("blob", new byte[] { 1, 2, 3 });
lua.Globals.Set("settings", settings);
using LuauTable roundTripped = lua.Globals.GetLuauTable("settings");
double volume = roundTripped.GetNumber("volume");
bool muted = roundTripped.GetBoolean("muted");
byte[] blob = roundTripped.GetBuffer("blob");Use Get* for required values, TryGet* for optional or external data, and *OrNil when nil is part of the contract.
[LuauUserdata]
public sealed partial class Player
{
[LuauMember("name", Access = LuauPropertyAccess.ReadOnly)]
public required string Name { get; init; }
}
var player = new Player { Name = "Ada" };
lua.Globals.Set("player", IntoLuau.FromUserdata(player));
Player samePlayer = lua.Globals.GetUserdata<Player>("player");
using LuauUserdata playerRef = lua.Globals.GetLuauUserdata("player");
_ = playerRef.TryGetManaged(out Player? resolvedPlayer, out string? error);Prefer [LuauUserdata] for regular script-facing properties and methods. Use a manual ILuauUserData<T> implementation only when you need custom dispatch or behavior the generator cannot express. See Userdata for the full hook model.
CreateFunction(...) also supports managed userdata parameters and returns for generated [LuauUserdata] types and manual ILuauUserData<TSelf> implementations.
Prefer [LuauModule] for regular fixed host APIs:
[LuauModule("game")]
public static partial class GameModule
{
[LuauMember("answer")]
public static int Answer => 42;
[LuauMember("add")]
public static int Add(int left, int right) => left + right;
}
lua.RegisterModule(GameModule.ModuleName, GameModule.OnLoad);Use manual RegisterModule(...) when you need dynamic table construction or unsupported callback shapes:
lua.RegisterModule("game", static (state, in LuauTable module) =>
{
module.Set("answer", 42);
using LuauFunction add = state.CreateFunction((int a, int b) => a + b);
module.Set("add", add);
});Host modules are loaded from Luau with require("game"). Generated and manual modules share the same require context as file-backed script modules.
LuauTable,LuauFunction,LuauString,LuauBuffer,LuauUserdata, and reference-backedLuauValueare owned references and should be disposed.LuauTableView,LuauFunctionView,LuauStringView,LuauBufferView,LuauUserdataView, andLuauArgsare borrowed callback-scoped values.- Reference-backed values belong to one
LuauState; cross-state usage is invalid.
Load(...).Execute(...)is the script execution API today. If you want file-based execution, read the file yourself and pass its contents in.CreateFunction(...)is generator-backed and has no runtime fallback.LuauStateis not thread-safe.- Higher-level async/thread orchestration is not part of the current surface yet.