diff --git a/README.md b/README.md
index fd3035e2..32b1297b 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/sandbox/Benchmark/Benchmark.csproj b/sandbox/Benchmark/Benchmark.csproj
index 8ace8446..dd064769 100644
--- a/sandbox/Benchmark/Benchmark.csproj
+++ b/sandbox/Benchmark/Benchmark.csproj
@@ -15,6 +15,12 @@
+
+
diff --git a/sandbox/Benchmark/CoroutineTransferBenchmark.cs b/sandbox/Benchmark/CoroutineTransferBenchmark.cs
new file mode 100644
index 00000000..f0bc2acb
--- /dev/null
+++ b/sandbox/Benchmark/CoroutineTransferBenchmark.cs
@@ -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();
+ }
+
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ cancellation.Dispose();
+ state.Dispose();
+ }
+}
diff --git a/sandbox/Benchmark/GeneratedUserDataBenchmark.cs b/sandbox/Benchmark/GeneratedUserDataBenchmark.cs
new file mode 100644
index 00000000..27fccf23
--- /dev/null
+++ b/sandbox/Benchmark/GeneratedUserDataBenchmark.cs
@@ -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 ("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();
+ }
+
+ [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;
+}
diff --git a/sandbox/Benchmark/Program.cs b/sandbox/Benchmark/Program.cs
index 5999c35f..bcfce358 100644
--- a/sandbox/Benchmark/Program.cs
+++ b/sandbox/Benchmark/Program.cs
@@ -29,6 +29,8 @@
typeof(InterpreterComparison),
typeof(CompilationComparison),
typeof(StartupComparison),
+ typeof(GeneratedUserDataBenchmark),
+ typeof(CoroutineTransferBenchmark),
])
.Run(args);
diff --git a/sandbox/Benchmark/README.md b/sandbox/Benchmark/README.md
index d45b9fab..9f441ce1 100644
--- a/sandbox/Benchmark/README.md
+++ b/sandbox/Benchmark/README.md
@@ -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
@@ -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*'
```
diff --git a/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs b/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs
index 567b89b3..fc3fc078 100644
--- a/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs
+++ b/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs
@@ -236,10 +236,7 @@ TempCollections tempCollections
metamethodSet.Add(LuaObjectMetamethod.NewIndex);
- if (!TryEmitMetatable(builder, metamethodSet, context))
- {
- return false;
- }
+ EmitMetatable(builder, metamethodSet);
// implicit operator
builder.AppendLine(
@@ -1342,62 +1339,74 @@ static void EmitMethodFunctionBody(
}
}
- static bool TryEmitMetatable(
- CodeBuilder builder,
- IEnumerable metamethods,
- in SourceProductionContext context
- )
+ static void EmitMetatable(CodeBuilder builder, IEnumerable 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