Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
09041b5
fix: validate bytecode and release VM state safely
akeit0 Oct 28, 2026
e6318a4
fix: separate loaded chunk initialization from closure capture
akeit0 Oct 28, 2026
f4ae4ad
fix: encode closure prototype indices as unsigned operands
akeit0 Oct 28, 2026
c630fe5
fix: release retained runtime references and align Lua 5.2 loading
akeit0 Oct 28, 2026
5ea35a3
fix: release cancelled coroutines and repair runtime diagnostics
akeit0 Oct 28, 2026
5f7152f
fix: own reader buffers and finish suspended coroutine cleanup
akeit0 Oct 28, 2026
417e159
fix: unwind failed calls and preserve buffered stream reads
akeit0 Oct 28, 2026
44c24a3
fix: reclaim deleted keys and complete runtime cleanup safely
akeit0 Oct 28, 2026
4dbc1d7
fix: fill omitted parameters and release failed host calls
akeit0 Oct 28, 2026
656d5d0
fix: reclaim tail-call arguments and correct hook frames
akeit0 Oct 28, 2026
a6e817b
fix: bound debug locals and release discarded call references
akeit0 Oct 28, 2026
f20f405
fix: release abandoned coroutine captures and correct wrap frames
akeit0 Oct 28, 2026
0ea5792
fix: preserve return-hook frames and isolate file metatables
akeit0 Oct 28, 2026
abebb71
fix: use stable Lua identities and release closed resources
akeit0 Oct 28, 2026
29546d8
fix: release pending exceptions after outermost unwind
akeit0 Oct 28, 2026
1b8657c
fix: release failed concat operands and pre-frame exceptions
akeit0 Oct 29, 2026
4b4b048
fix: reclaim comparison results and scope generated metatables
akeit0 Oct 29, 2026
72868a2
fix: freeze generated userdata metatables
akeit0 Oct 29, 2026
36c9d92
fix: reclaim variadic metamethod arguments
akeit0 Oct 29, 2026
52d6c26
fix: release coroutine transfers and preserve suspended yields
akeit0 Oct 29, 2026
17e00e2
fix: protect coroutine yield transfers from cancellation
akeit0 Oct 29, 2026
26e85a0
fix: preserve runtime state when argument transfers overflow
akeit0 Oct 29, 2026
6a3578d
fix: propagate coroutine resume cancellation and simplify handoffs
akeit0 Oct 29, 2026
25a7b02
refactor: trim coroutine cancellation overhead and redundant test cases
akeit0 Oct 29, 2026
18f635c
perf: preserve table length caching and spread numeric hashes
akeit0 Oct 29, 2026
56109f4
perf: streamline buffered stream reads
akeit0 Oct 29, 2026
3be7afc
test: trim overlapping metamethod and metatable cases
akeit0 Oct 29, 2026
4a9f072
refactor: share managed call and debug hook setup
akeit0 Oct 29, 2026
2d82534
perf: avoid temporary strings when reading stream numbers
akeit0 Oct 29, 2026
806beb4
perf: bulk copy serialized instruction and line arrays
akeit0 Oct 29, 2026
9ead679
refactor: remove redundant metatable emission failure path
akeit0 Oct 29, 2026
a0a933d
chore: format and document runtime benchmarks
akeit0 Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,8 @@ print(v1 - v2) -- <-1, -1, -1>
> [!NOTE]
> `__index` and `__newindex` cannot be set as they are used internally by the code generated by `[LuaObject]`.

Generated default metatables are immutable and shared by all instances of the same C# type, across Lua states. You can still assign writable C# members from Lua. To customize an individual object's metatable, replace `ILuaUserData.Metatable` in C# or use `debug.setmetatable` in Lua; replacement tables remain mutable. Ordinary Lua tables and metatables remain mutable.

## Module Loading

In Lua, you can load modules using the `require` function. In regular Lua, modules are managed by searchers within the `package.searchers` function list. In addition to this, Lua-CSharp provides `ILuaModuleLoader` as a module loading mechanism.
Expand Down
6 changes: 6 additions & 0 deletions sandbox/Benchmark/Benchmark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@

<ItemGroup>
<ProjectReference Include="..\..\src\Lua\Lua.csproj" />
<ProjectReference Include="..\..\src\Lua.Annotations\Lua.Annotations.csproj" />
<ProjectReference
Include="..\..\src\Lua.SourceGenerator\Lua.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
/>
</ItemGroup>

<ItemGroup>
Expand Down
61 changes: 61 additions & 0 deletions sandbox/Benchmark/CoroutineTransferBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using BenchmarkDotNet.Attributes;
using Lua;
using Lua.Runtime;
using Lua.Standard;

[Config(typeof(ComparisonConfig))]
public class CoroutineTransferBenchmark
{
[Params("resume", "wrap")]
public string Operation { get; set; } = "resume";

[Params(1, 64, 4096)]
public int ResultCount { get; set; }

[Params(false, true)]
public bool Cancellable { get; set; }

LuaState state = default!;
LuaClosure closure = default!;
CancellationTokenSource cancellation = default!;

[GlobalSetup]
public void Setup()
{
state = LuaState.Create();
state.OpenStandardLibraries();
cancellation = new();
state.Environment["packet"] = new LuaFunction(
(context, _) =>
{
context.GetReturnBuffer(ResultCount).Fill(42);
return new(ResultCount);
}
);
closure = state.Load(
Operation == "wrap"
? "local run=coroutine.wrap(function() for i=1,64 do coroutine.yield(packet()) end end); for i=1,64 do run() end; run(); return 42"
: "local co=coroutine.create(function() for i=1,64 do coroutine.yield(packet()) end end); for i=1,64 do coroutine.resume(co) end; coroutine.resume(co); return 42",
"coroutine-transfer"
);
if (Run() != 42)
throw new InvalidOperationException("Unexpected coroutine transfer result");
}

[Benchmark(OperationsPerInvoke = 64)]
public int Run()
{
state
.RunAsync(closure, 0, Cancellable ? cancellation.Token : default)
.GetAwaiter()
.GetResult();
return state.Stack.Pop().Read<int>();
}

[GlobalCleanup]
public void Cleanup()
{
cancellation.Dispose();
state.Dispose();
}
}
71 changes: 71 additions & 0 deletions sandbox/Benchmark/GeneratedUserDataBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using BenchmarkDotNet.Attributes;
using Lua;
using Lua.Runtime;

[Config(typeof(ComparisonConfig))]
public class GeneratedUserDataBenchmark
{
[Params("field", "method", "comparison", "write")]
public string Case { get; set; } = "field";

[Params(false, true)]
public bool OverrideMetatable { get; set; }

LuaState state = default!;
LuaClosure closure = default!;

[GlobalSetup]
public void Setup()
{
state = LuaState.Create();
var first = new GeneratedBenchmarkObject { Value = 7 };
var second = new GeneratedBenchmarkObject { Value = 8 };
if (OverrideMetatable)
{
ILuaUserData left = first;
ILuaUserData right = second;
left.Metatable = left.Metatable;
right.Metatable = right.Metatable;
}
state.Environment["first"] = first;
state.Environment["second"] = second;
var (source, expected) = Case switch
{
"field" => ("local v=first;local n=0;for i=1,1000 do n=n+v.Value end;return n", 7000),
"method" => ("local v=first;local n=0;for i=1,1000 do n=n+v:Add(1) end;return n", 8000),
"comparison" => (
"local a,b=first,second;local n=0;for i=1,1000 do if a<b then n=n+1 end end;return n",
1000
),
"write" => ("local v=first;for i=1,1000 do v.Value=i end;return v.Value", 1000),
_ => throw new ArgumentOutOfRangeException(nameof(Case)),
};
closure = state.Load(source, Case);
if (Run() != expected)
throw new InvalidOperationException($"Unexpected result for {Case}");
}

[Benchmark(OperationsPerInvoke = 1000)]
public int Run()
{
state.RunAsync(closure).GetAwaiter().GetResult();
return state.Stack.Pop().Read<int>();
}

[GlobalCleanup]
public void Cleanup() => state.Dispose();
}

[LuaObject]
public partial class GeneratedBenchmarkObject
{
[LuaMember]
public int Value { get; set; }

[LuaMember]
public int Add(int value) => Value + value;

[LuaMetamethod(LuaObjectMetamethod.Lt)]
public static bool Less(GeneratedBenchmarkObject left, GeneratedBenchmarkObject right) =>
left.Value < right.Value;
}
2 changes: 2 additions & 0 deletions sandbox/Benchmark/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
typeof(InterpreterComparison),
typeof(CompilationComparison),
typeof(StartupComparison),
typeof(GeneratedUserDataBenchmark),
typeof(CoroutineTransferBenchmark),
])
.Run(args);

Expand Down
4 changes: 4 additions & 0 deletions sandbox/Benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ measure Unicode encoding or arbitrary binary strings.
| `InterpreterComparison` | Execute a retained compiled chunk and read one numeric result | Numeric loops, Lua calls/varargs, table access/sort, metamethods, math, strings, patterns, coroutines, host callbacks, n-body |
| `CompilationComparison` | Compile source in an existing state; no execution | Small loop, n-body, control flow, identifiers, plain strings, escaped strings |
| `StartupComparison` | Create a state, open libraries, dispose where supported | One lifecycle per engine |
| `GeneratedUserDataBenchmark` | Execute 1,000 generated C# member accesses or comparisons; report time per operation | Property reads/writes, method calls, comparisons, with default and explicitly replaced metatables |
| `CoroutineTransferBenchmark` | Transfer 64 yielded packets; report time per transfer | `resume` and `wrap`, 1–4,096 results per packet, with and without cancellation |

Only the engine being measured is initialized for a benchmark. Execution
excludes source compilation and state setup. Scripts rebuild their mutable
Expand Down Expand Up @@ -52,6 +54,8 @@ equivalent deterministic disposal API.
dotnet bin/Release/net10.0/Benchmark.dll --filter '*InterpreterComparison*host-calls*'
dotnet bin/Release/net10.0/Benchmark.dll --filter '*CompilationComparison*'
dotnet bin/Release/net10.0/Benchmark.dll --filter '*StartupComparison*'
dotnet bin/Release/net10.0/Benchmark.dll --filter '*GeneratedUserDataBenchmark*'
dotnet bin/Release/net10.0/Benchmark.dll --filter '*CoroutineTransferBenchmark*'
dotnet bin/Release/net10.0/Benchmark.dll --legacy --filter '*GSubBenchmark*'
```

Expand Down
87 changes: 48 additions & 39 deletions src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,7 @@ TempCollections tempCollections

metamethodSet.Add(LuaObjectMetamethod.NewIndex);

if (!TryEmitMetatable(builder, metamethodSet, context))
{
return false;
}
EmitMetatable(builder, metamethodSet);

// implicit operator
builder.AppendLine(
Expand Down Expand Up @@ -1342,62 +1339,74 @@ static void EmitMethodFunctionBody(
}
}

static bool TryEmitMetatable(
CodeBuilder builder,
IEnumerable<LuaObjectMetamethod> metamethods,
in SourceProductionContext context
)
static void EmitMetatable(CodeBuilder builder, IEnumerable<LuaObjectMetamethod> metamethods)
{
builder.AppendLine("global::Lua.LuaTable? global::Lua.ILuaUserData.Metatable");
using (builder.BeginBlockScope())
{
builder.AppendLine("get => Metatable;");
builder.AppendLine("set => Metatable = value;");
}
builder.AppendLine("static global::Lua.LuaTable? Metatable");
using (builder.BeginBlockScope())
{
builder.AppendLine("get");
using (builder.BeginBlockScope())
{
builder.AppendLine(
"var metatable = global::System.Threading.Volatile.Read(ref __metatable);"
);
builder.AppendLine("if (metatable != null) return metatable;");
builder.AppendLine();
builder.AppendLine("metatable = new();");
foreach (var metamethod in metamethods)
{
var metaMethodName = metamethod.ToString();
var lowerName = metaMethodName.ToLower();
builder.AppendLine(
$"metatable[global::Lua.Runtime.Metamethods.{metaMethodName}] = __metamethod_{lowerName};"
);
}

builder.AppendLine(
"// Publish only a complete table. Concurrent readers share the winning instance,"
"var overrides = global::System.Threading.Volatile.Read(ref __metatableOverrides);"
);
builder.AppendLine(
"// including a custom metatable installed while initialization was in progress."
"return overrides != null && overrides.TryGetValue(this, out var entry) ? entry.Value : __metatable;"
);
}
builder.AppendLine("set");
using (builder.BeginBlockScope())
{
builder.AppendLine(
"return global::System.Threading.Interlocked.CompareExchange(ref __metatable, metatable, null) ?? metatable;"
"__GetMetatableOverrides().GetValue(this, _ => new __MetatableOverride(value)).Value = value;"
);
}
}

builder.AppendLine("set");
using (builder.BeginBlockScope())
builder.AppendLine("static global::Lua.LuaTable __CreateMetatable()");
using (builder.BeginBlockScope())
{
builder.AppendLine("var metatable = new global::Lua.LuaTable();");
foreach (var metamethod in metamethods)
{
var metaMethodName = metamethod.ToString();
var lowerName = metaMethodName.ToLower();
builder.AppendLine(
"global::System.Threading.Volatile.Write(ref __metatable, value);"
$"metatable[global::Lua.Runtime.Metamethods.{metaMethodName}] = __metamethod_{lowerName};"
);
}
builder.AppendLine("metatable.Freeze();");
builder.AppendLine("return metatable;");
}

builder.AppendLine("static global::Lua.LuaTable? __metatable;");
builder.AppendLine("sealed class __MetatableOverride");
using (builder.BeginBlockScope())
{
builder.AppendLine("public volatile global::Lua.LuaTable? Value;");
builder.AppendLine(
"public __MetatableOverride(global::Lua.LuaTable? value) => Value = value;"
);
}
builder.AppendLine(
"static global::System.Runtime.CompilerServices.ConditionalWeakTable<object, __MetatableOverride>? __metatableOverrides;"
);
builder.AppendLine(
"static global::System.Runtime.CompilerServices.ConditionalWeakTable<object, __MetatableOverride> __GetMetatableOverrides()"
);
using (builder.BeginBlockScope())
{
builder.AppendLine(
"var overrides = global::System.Threading.Volatile.Read(ref __metatableOverrides);"
);
builder.AppendLine("if (overrides != null) return overrides;");
builder.AppendLine("overrides = new();");
builder.AppendLine(
"return global::System.Threading.Interlocked.CompareExchange(ref __metatableOverrides, overrides, null) ?? overrides;"
);
}
builder.AppendLine(
"static readonly global::Lua.LuaTable __metatable = __CreateMetatable();"
);
builder.AppendLine();

return true;
}
}
Loading
Loading