Open-source project
NLua/NLua avatar
NLua/NLua

NLua: putting a Lua VM inside your .NET process, defaults and all

Bridge between Lua and the .NET.

2,245 stars325 forksC#MIT

At a glance

What is it?
NLua embeds Lua in .NET through the KeraLua binding and lets a script call any public .NET member with no registration step, which is its main convenience and its main hazard. Strings default to ASCII unless you switch the state encoding, and the documented sandbox is a plain Lua rewrite of import.
Who is it for?
Adopt NLua when your application must run scripts written by users, plugins written by a team, or an existing Lua codebase inside a .NET process, and you value zero-registration access to your own types over an isolation boundary. That last point is the decision: any public member on any assembly you import is reachable from a script, so untrusted script authors are a different conversation from trusted ones.
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 28 days 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 September 28, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What NLua is, and where it came from

NLua embeds a Lua interpreter inside a .NET process and lets code on either side call the other. It is a fork of LuaInterface, which came from Fabio Mascarenhas and Craig Presti, and the README is explicit about that lineage rather than presenting itself as a from-scratch binding.

The actual native layer is KeraLua, a separate project maintained alongside NLua and consumed as its binding. That layering matters when something goes wrong: a crash inside the interpreter is usually a KeraLua question, and the boundary between managed marshalling and the native VM is where the interesting failure modes live.

The project targets more than one runtime family. The README describes compatibility with .NET, UWP, Windows, Linux, Mac, iOS, Android and tvOS, and the repository backs that up with separate solution files: NLua.net8.0.sln, NLua.netstandard2.0.sln, NLua.Android.sln, NLua.Mac.sln and NLua.UWP.sln, plus a strong-name key file and NuGet specification files. Source builds with msbuild NLua.sln.

The result is a component rather than a framework: you add a package, create a state, and hand out capability by what you choose to expose.

No registration: any public .NET member is reachable from Lua

The claim that leads the README is that you can use or instantiate any .NET class with no previous registration or annotation. That is the reason to choose this binding over one where every type has to be declared first, and it is also the decision that deserves scrutiny.

Passing a value is assignment. A plain object goes into the state under a global name:

csharp
SomeClass obj = new SomeClass ("Param");
state ["obj"] = obj;

From Lua, instance methods use colon notation and static methods use dot notation on the class name, with property get and set also using dot:

csharp
state.DoString (@"
	local res1 = obj:Func1()
	local res2 = obj2:AnotherFunc (10, 'hello')
	local res3 = client:DownloadString('http://nlua.org')
");
csharp
state.DoString (@"
local res4 = SomeClass.StaticMethod(4)
");

To reach types the script has not been handed, you enable the CLR package and import assemblies by name:

csharp
state.LoadCLRPackage ();
state.DoString (@" import ('MyAssembly', 'MyNamespace') 
		   import ('System.Web') ");

Once import runs, the script can construct objects with the class name and parentheses, for example SomeClass() or WebClient(). The README states the limit plainly: all methods, events or properties must be public, because NLua cannot call non-public members. That is a visibility rule, not a permission system.

Values in and out: globals, functions and multiple returns

Creating a state and evaluating an expression is the whole hello world:

csharp
using NLua;

Lua state = new Lua ()
csharp
var res = state.DoString ("return 10 + 3*(5 + 2)")[0] as double;

Note the trailing [0] and the reason the README gives for it: Lua can return multiple values, so DoString returns an array of objects. That detail recurs everywhere in this API and is where most bugs start.

Globals are written and read through the same indexer:

csharp
double val = 12.0;
state ["x"] = val;
var res = (double)state.DoString ("return 10 + x*(5 + 2)")[0];
csharp
state.DoString ("y = 10 + x*(5 + 2)");
double y = (double) state ["y"];

Functions retrieved from the state come back as LuaFunction, and calling them returns an array for the same reason:

csharp
var scriptFunc = state ["ScriptFunc"] as LuaFunction;
var res = (int)scriptFunc.Call (3, 5).First ();

Because the return type is object, casts are explicit at every boundary. A script returning a number and your C# code expecting an int will throw rather than convert quietly, which is safer than the alternative and noisier in logs.

Strings default to ASCII, and the README example proves it

The one documented gotcha is encoding. NLua runs on top of KeraLua and, as the README puts it, encodes strings using ASCII by default. To use UTF-8 you set the Encoding property on the state:

csharp
using (Lua lua = new Lua())
{
	lua.State.Encoding = Encoding.UTF8;
	lua.DoString("res = 'Файл'");
	string res = (string)lua["res"];

	Assert.AreEqual("Файл", res);
}

That example is worth reading twice. It uses a Cyrillic literal, it asserts the round trip, and it requires the encoding to be set before the script runs. Set the property after evaluating a script and the damage is already done for the values already in the state.

This is not an exotic scenario for a binding used on Windows and in cross-platform applications. Any script that carries non-ASCII text, which means most user-facing text outside English, depends on one line of C# being present in the right place. It is a reasonable default for a low-level binding and a permanent source of bugs for applications that forget it, and it is not configurable per call, only per state.

Building, packaging and where the platform caveats appear

Consumption is a NuGet package named NLua, and the README also points at downloading KeraLua and NLua directly from NuGet for the Unity route, where it asks for a .NET 2.0 build. That instruction is a good signal about reach: it still supports a hosting environment that predates .NET Standard. The most recent tag, v1.7.9, was published on 2026-05-01.

From source it is msbuild NLua.sln, with the platform-specific solutions available when you need them. The presence of NLua.Symbol.nuspec alongside NLua.nuspec indicates a symbols package, and NLua.snk is the strong-name key, so assemblies are signed.

The mobile caveat is specific and easy to hit. On Xamarin.iOS, the README says you must preserve any class you intend to use inside NLua, because otherwise the linker removes it from the final binary when nothing references it directly. In other words, a class that works in development can disappear in release, and the fix is an attribute rather than a code change.

Continuous integration is spread across three systems, with build badges for Linux on Travis, Windows on AppVeyor, and macOS, iOS, tvOS and Windows on Azure DevOps. For a library of this size that is more pipeline than code, and it is also why the build story takes a paragraph rather than a line.

Sandboxing: the documented technique is a Lua rewrite

If you accept scripts from people you do not trust, this is the section to read twice. The README says there are many ways to sandbox scripts inside your application, and that its strong recommendation is to use plain Lua for the sandbox.

The mechanism is one line of Lua evaluated before the untrusted script runs:

csharp
state.DoString (@"
		import = function () end
	");

Redefining import as a no-op means a script that tries to load a .NET assembly gets nothing back. It is an elegant, tiny technique with a clear target, and the README links to a Lua sandbox user list for further reading.

What it does not do is revoke capability already granted. Any object you placed in the state with state ["obj"] = obj stays callable with its public members, and any capability you obtained before the rewrite remains reachable. It also operates on the Lua global table rather than on the runtime, so it constrains the script language, not the process.

So the honest summary is: NLua can host untrusted scripts if you design for it and never hand the script a capability you would not give a stranger, and it will not protect you from a script that reaches an object you did hand it. For anything stricter you would need a separate process, and that is a design change rather than a configuration flag.

Alternatives: a separate Lua process, or KeraLua directly

There are two comparison points worth weighing, and both are already referenced by the project itself.

The first is not embedding Lua at all. NLua ships an extras command-line host, shown in the README screenshot, and a process boundary is the obvious alternative: run Lua outside your application, pass work over stdin and stdout or a socket, and treat every input as untrusted by construction. You give up in-process state, shared memory and the convenience of calling a .NET object directly, and you buy containment that no amount of Lua-level rewriting can provide.

The second is dropping a layer and using KeraLua directly, which the README names as the binding NLua sits on. KeraLua gives you the P/Invoke surface to the native VM without NLua's marshalling and its CLR package, so you control exactly what crosses the boundary and you write the conversion code. That is more work per call, and it is the right answer when your requirement is a narrow, fixed scripting surface rather than general access to your object model.

For a fixed, trusted script API, KeraLua plus your own wrapper functions is often the cleaner dependency. For broad object access with the convenience of colon and dot notation, NLua is the version of this problem that already exists.

Editorial conclusion

Adopt NLua when your application must run scripts written by users, plugins written by a team, or an existing Lua codebase inside a .NET process, and you value zero-registration access to your own types over an isolation boundary. That last point is the decision: any public member on any assembly you import is reachable from a script, so untrusted script authors are a different conversation from trusted ones. Verify four things first. Set lua.State.Encoding to Encoding.UTF8 before you evaluate anything, because the default is ASCII and the README's own example uses a Cyrillic string. Read the sandbox section before you load user input, because the recommended technique is redefining import in Lua, which removes one door and leaves the objects you already passed into the state reachable. Decide how you will consume return values, since DoString and LuaFunction.Call both hand back an array because Lua can return several values and taking index zero by default silently discards the rest. And check your target: the repository carries separate solutions for net8.0 and netstandard2.0 alongside UWP, Android and Mac ones, so confirm the package you restore matches the framework you build. Start by creating a Lua state, evaluating a single expression, and reading the result back before you wire any real type in.

Frequently asked questions

Do I need to register .NET classes with NLua before using them from Lua?

No. The README states you can use or instantiate any .NET class without registration or annotation, and objects are passed into the state by assigning them. All methods, events and properties must be public, since NLua cannot call non-public members.

How do I use UTF-8 strings with NLua?

Set lua.State.Encoding to Encoding.UTF8 before evaluating scripts, because the default encoding is ASCII. The README demonstrates a round trip with a Cyrillic string and an assertion on the result.

How do I call a .NET assembly from inside a Lua script?

Call state.LoadCLRPackage() from C#, then use the import function in the script, for example import ('MyAssembly', 'MyNamespace'). Loaded assemblies become available inside the Lua context.

How can I sandbox untrusted Lua scripts in NLua?

The README recommends plain Lua sandboxing and shows redefining import as a no-op before loading the user script, so assembly imports do nothing. Objects already placed in the state remain callable, so do not hand a script capability you would not give a stranger.

Why does my class disappear from an iOS build using NLua?

On Xamarin.iOS the linker removes classes that are not referenced, so you must preserve the class you want to use inside NLua. The README links to the Xamarin linker documentation for the Preserve step.

Official sources

  1. License: MIT
  2. NLua/NLua on GitHub
  3. Project website
  4. README
  5. Releases