CLI tool
nuskey8/Lua-CSharp avatar
nuskey8/Lua-CSharp

Lua-CSharp: a Lua 5.2 interpreter written in C#

High performance Lua interpreter implemented in C# for .NET and Unity

873 stars76 forksC#MIT

At a glance

What is it?
Lua-CSharp is a Lua 5.2 interpreter implemented entirely in C# for .NET and Unity, published as the LuaCSharp NuGet package. The pitch is allocation-light interop with no IL generation, which keeps it usable under IL2CPP, and the cost is a partial standard library and a state object that is not thread-safe.
Who is it for?
Adopt Lua-CSharp when your .NET or Unity application needs to embed scripting and you want the interpreter to be ordinary managed code, because a pure C# implementation means no native binding layer, no IL emission, and therefore no AOT problem under IL2CPP.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly C#, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

An interpreter written in managed code, not bound to C

Lua-CSharp is a library that provides a Lua interpreter implemented in C#, and the repository describes it as a high performance Lua interpreter implemented in C# for .NET and Unity. The interesting part is not that it embeds Lua, but that it reimplements it in the host language rather than compiling the reference implementation and binding to it.

The README draws the comparison with the two alternatives explicitly. MoonSharp generally provides good speed but incurs significant allocations due to its design. NLua, being a C-binding implementation, is fast, but introduces substantial overhead when interacting with the C# layer. Lua-CSharp, fully implemented in C#, is presented as allowing direct interaction with C# code without that overhead. The comparison itself is a screenshot, docs/Benchmark.png, so the README shows a chart rather than numbers you can read as text, and the chart's test conditions are not documented anywhere in the repository.

The structural argument is stronger than the chart. Because the interpreter is managed code, it does not rely on IL generation, and that is what makes it operate reliably in AOT environments, where ahead-of-time compilation cannot emit code at runtime. For Unity that means the IL2CPP backend works as well as Mono, which the feature list states directly.

The features are short: a Lua 5.2 interpreter, an API integrated with async and await, exception handling with try-catch, a high-performance implementation built on modern C# features, and Unity support with both Mono and IL2CPP. The licence is MIT, and the package is on NuGet as LuaCSharp.

Two package manager commands and a netstandard floor

Installing the library takes one command in either of the two package managers the README covers. From the .NET CLI:

ps1
dotnet add package LuaCSharp

And from the Package Manager console:

ps1
Install-Package LuaCSharp

The floor is .NET Standard 2.1 or higher, which is the requirement the README states for using the package. That is a fairly recent target, and it is worth noticing what it excludes: the package does not attempt to run on the older .NET Framework or on .NET Standard 2.0 targets, which matters if your application is on an older runtime or on a Mono profile that predates it.

The first script you would write is short. The LuaState class is the entry point, and DoStringAsync evaluates a script and returns the results:

cs
using Lua;

// Create a LuaState
var state = LuaState.Create();

// Execute a Lua script string with DoStringAsync
var results = await state.DoStringAsync("return 1 + 1");

// 2
Console.WriteLine(results[0]);

There is a warning attached to that class that deserves more attention than it usually gets in a README. LuaState is not thread-safe, and you should not access it from multiple threads simultaneously. That is a design constraint rather than a bug, and it shapes how the library can be used in a server application, where a single state per request or a lock around a shared state are the two obvious answers.

LuaValue, and the mapping between the two type systems

Every value that comes back from Lua is a LuaValue, and the README explains that the value can be read using TryRead<T>(out T value) or Read<T>(). The two-method pattern is familiar from other interop layers: TryRead is the one to use when the type might not match, Read is the one that will throw if it does.

The type mapping is laid out as a table, and it is the clearest statement of what the bridge does:

| Lua | C# | | ----------------- | ------------------------ | | `nil` | `LuaValue.Nil` | | `boolean` | `bool` | | `string` | `string` | | `number` | `double`, `float`, `int` | | `table` | `LuaTable` | | `function` | `LuaFunction` | | (light)`userdata` | `object` | | `userdata` | `ILuaUserData` | | `thread` | `LuaState` |

The last row is the one with a consequence for your design. A Lua thread is represented by a LuaState, which means coroutines are part of the model, but it also means the type of a value that a script returns can be an interpreter object, not a plain data value.

Reading a specific type and asking for the type are both one-liners:

cs
var results = await state.DoStringAsync("return 1 + 1");

// double
var value = results[0].Read<double>();
cs
var isNil = results[0].Type == LuaValueType.Nil;

Going the other way, the README notes that compatible types are implicitly converted into a LuaValue when you create one from the C# side, so a double, a string or a LuaTable can be assigned directly to a LuaValue variable without a conversion call.

LuaTable behaves like a dictionary and a one-indexed array

Tables are the only real data structure in Lua, and Lua-CSharp represents them with the LuaTable type, which the README says can be used similarly to LuaValue[] or to Dictionary<LuaValue, LuaValue>. That is a helpful description because it tells you which C# idioms will feel natural, but there is one detail that will bite anyone coming from C#.

Lua arrays are 1-indexed. The README marks it in a comment, and the sample shows it plainly: a table returned as { a = 1, b = 2, c = 3 } is read with a named key, while a table returned as { 1, 2, 3 } is read with index 1 for the first element.

cs
// Create a table in Lua
var results = await state.DoStringAsync("return { a = 1, b = 2, c = 3 }");
var table1 = results[0].Read<LuaTable>();

// 1
Console.WriteLine(table1["a"]);

// Create a table in C#
results = await state.DoStringAsync("return { 1, 2, 3 }");
var table2 = results[0].Read<LuaTable>();

// 1 (Note: Lua arrays are 1-indexed)
Console.WriteLine(table2[1]);

The other half of the model is the global environment, which you reach through state.Environment. It is a table, which means it supports the same dictionary access, and it is the natural place to hand values to a script and read values back out of it:

cs
// Set a = 10
state.Environment["a"] = 10;

var results = await state.DoStringAsync("return a");

// 10
Console.WriteLine(results[0]);

The README describes this table as the place for easy value exchange between Lua and C#, and the example shows the same global name being set from C# and then read by a script.

Calling across the boundary, with a stack API that avoids array allocation

Functions are represented by the LuaFunction type, and they work in both directions. A Lua function can be called from C#, and a C# function can be defined and called from Lua.

The Lua side is a plain Lua file, which the C# code loads and calls:

lua
-- lua2cs.lua

local function add(a, b)
    return a + b
end

return add;
cs
var state = LuaState.Create();
var results = await state.DoFileAsync("lua2cs.lua");
var func = results[0];

// Execute the function or any callable with arguments
var funcResults = await state.CallAsync(func, [1, 2]);

// 3
Console.WriteLine(funcResults[0]);

The first form passes arguments as an array, and the README points out that an API is also provided that passes arguments using the stack, to avoid array allocation. That variant works against a stack held on the state, and it returns a reader over the stack rather than a collection:

cs
var func = results[0];
var basePos = state.Stack.Count;
state.Push(func);
state.Push(1);
state.Push(2);
var funcResultsCount = await state.CallAsync(funcIndex: basePos, returnBase: basePos);

using (var reader = state.ReadStack(funcResultsCount))
{
    var span = reader.AsSpan();
    for (int i = 0; i < span.Length; i++)
    {
        Console.WriteLine(span[i]);
    }
}

Going the other way, a C# function is created from a lambda, and the callback receives a context and a cancellation token. Arguments are pulled out by index and the return value is written back through the same context:

cs
// Add the function to the global environment
state.Environment["add"] = new LuaFunction((context, ct) =>
{
    // Get the arguments using context.GetArgument<T>()
    var arg0 = context.GetArgument<double>(0);
    var arg1 = context.GetArgument<double>(1);

    // Set the return value to the context
    context.Return(arg0 + arg1);

The cancellation token in that signature is worth noticing, because it is how a long-running script can be interrupted from the .NET side.

The standard library is partial, and that is the catch

Standard libraries are opt-in. You call state.OpenStandardLibraries() and the standard library tables are added to the LuaState, and the sample then reads math.pi out of the script:

cs
using Lua;
using Lua.Standard;

var state = LuaState.Create();

// Add standard libraries
state.OpenStandardLibraries();

var results = await state.DoStringAsync("return math.pi");
Console.WriteLine(results[0]); // 3.141592653589793

Underneath that sample sits the warning that matters for a project that is choosing this library: Lua-CSharp does not support all functions of the standard libraries, and the README sends you to a compatibility section for the details. The separate using for Lua.Standard is the visible hint of the split, since the standard library is a separate namespace from the core types.

The reference the README gives for the semantics of the libraries themselves is the Lua 5.2 manual, which is the right starting point for behaviour, and the compatibility section is the one to check for coverage. Scripts written against the reference implementation may call a function that simply is not there, and the failure will surface at runtime in the script rather than at compile time in the host application.

Everything else about the packaging is conventional. The project targets Lua 5.2 rather than a newer version, so a script written for 5.3 or 5.4 with its integer division or its goto may not behave as expected, and the README does not discuss version differences.

The repository layout and where the version line stands

The top-level directory of the repository is a normal modern .NET solution with some unusual entries. There is a Directory.Build.props for shared build properties, a Lua.slnx solution file, a dotnet-tools.json for tool manifest pins, a .csharpierrc.json for the formatter, an AGENTS.md and a CLAUDE.md next to a CONTRIBUTING.md, plus src/, tests/, sandbox/ and docs/. The sandbox directory is the one worth checking, because it is where a runnable playground of the interpreter would live.

The version line tells you where the project stands. The releases are v0.5.5 on 2026-05-09, v0.5.6 on 2026-07-29 and v0.5.7 on 2026-09-26, so the package is on the 0.5 series and the releases land every couple of months. The last push to the repository was on 2026-10-14, and the repository is not archived, so the code is moving.

Two things to settle before you depend on it. The first is the version you pin, since a 0.5 package can still change shape between minor versions, and the second is the standard library coverage for the calls your scripts make, because that is the limitation the project itself flags, and it is invisible until a script calls something that is missing.

Editorial conclusion

Adopt Lua-CSharp when your .NET or Unity application needs to embed scripting and you want the interpreter to be ordinary managed code, because a pure C# implementation means no native binding layer, no IL emission, and therefore no AOT problem under IL2CPP. Do not adopt it if you need a complete Lua standard library, since the README warns that not every standard library function is supported and points to the compatibility section, and do not share a single LuaState across threads, because it is explicitly not thread-safe. Before you commit, read that compatibility section for the functions you actually call, decide whether the async API or the stack API suits your call sites, and pin the NuGet version, since the package is still on the 0.5 line with v0.5.7 published on 2026-09-26.

Frequently asked questions

How do I install Lua-CSharp?

The package is on NuGet as LuaCSharp. From the .NET CLI you run dotnet add package LuaCSharp, and from the Package Manager console you run Install-Package LuaCSharp. The README states that .NET Standard 2.1 or higher is required.

Can Lua-CSharp be used with Unity?

Yes. The feature list states Unity support, and it works with both Mono and IL2CPP. The README gives the reason in one line: because the interpreter is fully implemented in C# and does not rely on IL generation, it operates reliably in AOT environments.

Is LuaState safe to use from multiple threads?

No. The README carries an explicit warning that LuaState is not thread-safe and should not be accessed from multiple threads simultaneously, so a host application has to give each thread its own state or synchronise access to a shared one.

Does Lua-CSharp support the whole Lua standard library?

No. The README warns that Lua-CSharp does not support all functions of the standard libraries and refers you to its compatibility section, while the standard library is loaded on demand through state.OpenStandardLibraries() and lives in the Lua.Standard namespace.

Official sources

  1. Issues
  2. License: MIT
  3. nuskey8/Lua-CSharp on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/nuskey8-lua-csharp.svg)](https://hysenlabs.com/projects/nuskey8-lua-csharp)

Was this useful?