Functions¶
Functions are one of the main ways managed code and Luau call into each other.
You usually work in two directions:
- call an existing Luau function from C#,
- expose a managed callback so Luau can call into your code.
Call Luau functions from C#¶
Get an owned LuauFunction from globals or a table, keep it for as long as needed, and invoke it with typed arguments:
using LuauFunction add = lua.Globals.GetLuauFunction("add");
double result = add.Invoke<double>(1, 2);
using LuauFunction pair = lua.Globals.GetLuauFunction("pair");
(int sum, int difference) = pair.Invoke<int, int>(20, 4);
The generic return type controls how the Luau return value is converted.
- Use
Invoke(...)when you want to ignore return values. - Use
Invoke<TR>(...)for a single typed return value. Extra Luau return values are ignored. - Use
Invoke<TR1, TR2>(...)orInvoke<TR1, TR2, TR3>(...)when you want multiple typed return values. - Use
InvokeMulti(...)when you want all Luau return values as rawLuauValueinstances. - Argument values go through the normal
IntoLuauconversion rules. - The current argument buffer accepts up to 4 arguments per call.
If Luau raises an error, Invoke<TR>(...) throws LuaException. If the return value cannot be converted to TR, it throws InvalidCastException.
Expose managed callbacks with CreateFunction(...)¶
Use CreateFunction(...) for supported fixed delegate signatures:
using LuauFunction log = lua.CreateFunction((string message) => Console.WriteLine(message));
lua.Globals.Set("log", log);
using LuauFunction add = lua.CreateFunction((int a, int b) => a + b);
lua.Globals.Set("add", add);
using LuauFunction pair = lua.CreateFunction((int a, int b) => (a + b, a - b));
lua.Globals.Set("pair", pair);
This is the normal callback API when your callback shape is simple and static.
Direct-call requirement¶
CreateFunction(...) is generator-backed. The direct method call is intercepted at compile time and replaced with a generated marshalling adapter.
- Call
CreateFunction(...)directly at the call site. - Do not store
lua.CreateFunctionin another delegate and invoke it indirectly. - There is no runtime fallback. If interception does not happen, the stub throws.
What fits this API well¶
CreateFunction(...) is best for fixed signatures with:
- primitive numeric and boolean types,
- supported nullable value types,
- enums,
stringand span-based string or buffer parameters,LuauValue,- managed userdata types, either generated with
[LuauUserdata]or implemented manually withILuauUserData<TSelf>, - borrowed callback views such as
LuauTableVieworLuauFunctionView, void, one managed return value, or a top-level tuple return whose elements are individually supported.
Managed userdata support here is the typed managed path, not the raw userdata wrapper path. Use LuauUserdataView when you want a borrowed userdata view directly; use [LuauUserdata] or ILuauUserData<TSelf> when you want CreateFunction(...) to marshal to and from your managed type.
The supported signature set is narrower than the library's overall type-conversion surface. Generator-backed callbacks currently reject nested tuple returns and are limited to top-level tuple returns that fit the current LuauReturn.Ok(...) arity. If a delegate shape is not supported there, use CreateFunctionBuilder(...) instead. See Type mapping for the broader conversion model.
Choose between callback APIs¶
| Capability | CreateFunction(...) |
CreateFunctionBuilder(...) |
|---|---|---|
| Input shape | typed delegate | LuauArgs |
| Output shape | void, one managed return value, or a supported top-level tuple return |
LuauReturn.Ok(...) / LuauReturn.Error(...) |
| Requirements | direct call, generator-backed | plain runtime API |
| Best for | simple fixed signatures, including supported tuple returns | manual validation, custom errors, unsupported signatures |
Prefer CreateFunction(...) unless you specifically need the extra control from CreateFunctionBuilder(...).
Use CreateFunctionBuilder(...) for manual callbacks¶
Use CreateFunctionBuilder(...) when you want to parse callback arguments yourself, shape the user-facing error contract explicitly, or expose a callback shape the generator 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);
});
lua.Globals.Set("pair", pair);
TryValidateArgumentCount(...) checks minimum arity, not exact arity. If your callback requires an exact argument count, compare args.ArgumentCount yourself as well.
Borrowed callback values¶
LuauArgs can return borrowed callback-scoped views such as LuauFunctionView, LuauTableView, LuauStringView, LuauBufferView, and LuauUserdataView.
Use them immediately, or promote them to owned references before the callback returns:
using LuauFunction invokeCallback = lua.CreateFunctionBuilder(static args =>
{
if (!args.TryReadLuauFunction(1, out LuauFunctionView callback, out string? error))
return LuauReturn.Error(error);
using LuauFunction owned = callback.ToOwned();
owned.Invoke();
return LuauReturn.Ok();
});
Borrowed views are valid only while the current callback frame is active. See Lifetimes and ownership. For string- and buffer-specific callback shapes and ownership rules, see Strings and Buffers.
Error behavior¶
Callback failures become normal Luau errors:
- argument conversion failures become Luau errors,
LuauReturn.Error(...)produces a Luau error with your message,- thrown exceptions are caught and surfaced as managed callback failures,
pcall(...)can catch those errors on the Luau side.
Signature guidance¶
- Keep callback signatures narrow and explicit.
- Make nullable behavior intentional.
- Prefer plain managed values for stable contracts.
- Use
CreateFunctionBuilder(...)when you need fine-grained validation instead of hiding it behind a wide delegate signature.