Skip to content

Commit 94de422

Browse files
authored
Merge pull request #391 from nuskey8/codex/memory-safety-fixes
Fix runtime memory lifetimes and validate loaded bytecode
2 parents a8d84c7 + a0a933d commit 94de422

79 files changed

Lines changed: 7946 additions & 1200 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,8 @@ print(v1 - v2) -- <-1, -1, -1>
537537
> [!NOTE]
538538
> `__index` and `__newindex` cannot be set as they are used internally by the code generated by `[LuaObject]`.
539539
540+
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.
541+
540542
## Module Loading
541543

542544
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.

‎sandbox/Benchmark/Benchmark.csproj‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515

1616
<ItemGroup>
1717
<ProjectReference Include="..\..\src\Lua\Lua.csproj" />
18+
<ProjectReference Include="..\..\src\Lua.Annotations\Lua.Annotations.csproj" />
19+
<ProjectReference
20+
Include="..\..\src\Lua.SourceGenerator\Lua.SourceGenerator.csproj"
21+
OutputItemType="Analyzer"
22+
ReferenceOutputAssembly="false"
23+
/>
1824
</ItemGroup>
1925

2026
<ItemGroup>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using BenchmarkDotNet.Attributes;
2+
using Lua;
3+
using Lua.Runtime;
4+
using Lua.Standard;
5+
6+
[Config(typeof(ComparisonConfig))]
7+
public class CoroutineTransferBenchmark
8+
{
9+
[Params("resume", "wrap")]
10+
public string Operation { get; set; } = "resume";
11+
12+
[Params(1, 64, 4096)]
13+
public int ResultCount { get; set; }
14+
15+
[Params(false, true)]
16+
public bool Cancellable { get; set; }
17+
18+
LuaState state = default!;
19+
LuaClosure closure = default!;
20+
CancellationTokenSource cancellation = default!;
21+
22+
[GlobalSetup]
23+
public void Setup()
24+
{
25+
state = LuaState.Create();
26+
state.OpenStandardLibraries();
27+
cancellation = new();
28+
state.Environment["packet"] = new LuaFunction(
29+
(context, _) =>
30+
{
31+
context.GetReturnBuffer(ResultCount).Fill(42);
32+
return new(ResultCount);
33+
}
34+
);
35+
closure = state.Load(
36+
Operation == "wrap"
37+
? "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"
38+
: "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",
39+
"coroutine-transfer"
40+
);
41+
if (Run() != 42)
42+
throw new InvalidOperationException("Unexpected coroutine transfer result");
43+
}
44+
45+
[Benchmark(OperationsPerInvoke = 64)]
46+
public int Run()
47+
{
48+
state
49+
.RunAsync(closure, 0, Cancellable ? cancellation.Token : default)
50+
.GetAwaiter()
51+
.GetResult();
52+
return state.Stack.Pop().Read<int>();
53+
}
54+
55+
[GlobalCleanup]
56+
public void Cleanup()
57+
{
58+
cancellation.Dispose();
59+
state.Dispose();
60+
}
61+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using BenchmarkDotNet.Attributes;
2+
using Lua;
3+
using Lua.Runtime;
4+
5+
[Config(typeof(ComparisonConfig))]
6+
public class GeneratedUserDataBenchmark
7+
{
8+
[Params("field", "method", "comparison", "write")]
9+
public string Case { get; set; } = "field";
10+
11+
[Params(false, true)]
12+
public bool OverrideMetatable { get; set; }
13+
14+
LuaState state = default!;
15+
LuaClosure closure = default!;
16+
17+
[GlobalSetup]
18+
public void Setup()
19+
{
20+
state = LuaState.Create();
21+
var first = new GeneratedBenchmarkObject { Value = 7 };
22+
var second = new GeneratedBenchmarkObject { Value = 8 };
23+
if (OverrideMetatable)
24+
{
25+
ILuaUserData left = first;
26+
ILuaUserData right = second;
27+
left.Metatable = left.Metatable;
28+
right.Metatable = right.Metatable;
29+
}
30+
state.Environment["first"] = first;
31+
state.Environment["second"] = second;
32+
var (source, expected) = Case switch
33+
{
34+
"field" => ("local v=first;local n=0;for i=1,1000 do n=n+v.Value end;return n", 7000),
35+
"method" => ("local v=first;local n=0;for i=1,1000 do n=n+v:Add(1) end;return n", 8000),
36+
"comparison" => (
37+
"local a,b=first,second;local n=0;for i=1,1000 do if a<b then n=n+1 end end;return n",
38+
1000
39+
),
40+
"write" => ("local v=first;for i=1,1000 do v.Value=i end;return v.Value", 1000),
41+
_ => throw new ArgumentOutOfRangeException(nameof(Case)),
42+
};
43+
closure = state.Load(source, Case);
44+
if (Run() != expected)
45+
throw new InvalidOperationException($"Unexpected result for {Case}");
46+
}
47+
48+
[Benchmark(OperationsPerInvoke = 1000)]
49+
public int Run()
50+
{
51+
state.RunAsync(closure).GetAwaiter().GetResult();
52+
return state.Stack.Pop().Read<int>();
53+
}
54+
55+
[GlobalCleanup]
56+
public void Cleanup() => state.Dispose();
57+
}
58+
59+
[LuaObject]
60+
public partial class GeneratedBenchmarkObject
61+
{
62+
[LuaMember]
63+
public int Value { get; set; }
64+
65+
[LuaMember]
66+
public int Add(int value) => Value + value;
67+
68+
[LuaMetamethod(LuaObjectMetamethod.Lt)]
69+
public static bool Less(GeneratedBenchmarkObject left, GeneratedBenchmarkObject right) =>
70+
left.Value < right.Value;
71+
}

‎sandbox/Benchmark/Program.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
typeof(InterpreterComparison),
3030
typeof(CompilationComparison),
3131
typeof(StartupComparison),
32+
typeof(GeneratedUserDataBenchmark),
33+
typeof(CoroutineTransferBenchmark),
3234
])
3335
.Run(args);
3436

‎sandbox/Benchmark/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ measure Unicode encoding or arbitrary binary strings.
2525
| `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 |
2626
| `CompilationComparison` | Compile source in an existing state; no execution | Small loop, n-body, control flow, identifiers, plain strings, escaped strings |
2727
| `StartupComparison` | Create a state, open libraries, dispose where supported | One lifecycle per engine |
28+
| `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 |
29+
| `CoroutineTransferBenchmark` | Transfer 64 yielded packets; report time per transfer | `resume` and `wrap`, 1–4,096 results per packet, with and without cancellation |
2830

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

‎src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs‎

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,7 @@ TempCollections tempCollections
236236

237237
metamethodSet.Add(LuaObjectMetamethod.NewIndex);
238238

239-
if (!TryEmitMetatable(builder, metamethodSet, context))
240-
{
241-
return false;
242-
}
239+
EmitMetatable(builder, metamethodSet);
243240

244241
// implicit operator
245242
builder.AppendLine(
@@ -1342,62 +1339,74 @@ static void EmitMethodFunctionBody(
13421339
}
13431340
}
13441341

1345-
static bool TryEmitMetatable(
1346-
CodeBuilder builder,
1347-
IEnumerable<LuaObjectMetamethod> metamethods,
1348-
in SourceProductionContext context
1349-
)
1342+
static void EmitMetatable(CodeBuilder builder, IEnumerable<LuaObjectMetamethod> metamethods)
13501343
{
13511344
builder.AppendLine("global::Lua.LuaTable? global::Lua.ILuaUserData.Metatable");
13521345
using (builder.BeginBlockScope())
1353-
{
1354-
builder.AppendLine("get => Metatable;");
1355-
builder.AppendLine("set => Metatable = value;");
1356-
}
1357-
builder.AppendLine("static global::Lua.LuaTable? Metatable");
1358-
using (builder.BeginBlockScope())
13591346
{
13601347
builder.AppendLine("get");
13611348
using (builder.BeginBlockScope())
13621349
{
13631350
builder.AppendLine(
1364-
"var metatable = global::System.Threading.Volatile.Read(ref __metatable);"
1365-
);
1366-
builder.AppendLine("if (metatable != null) return metatable;");
1367-
builder.AppendLine();
1368-
builder.AppendLine("metatable = new();");
1369-
foreach (var metamethod in metamethods)
1370-
{
1371-
var metaMethodName = metamethod.ToString();
1372-
var lowerName = metaMethodName.ToLower();
1373-
builder.AppendLine(
1374-
$"metatable[global::Lua.Runtime.Metamethods.{metaMethodName}] = __metamethod_{lowerName};"
1375-
);
1376-
}
1377-
1378-
builder.AppendLine(
1379-
"// Publish only a complete table. Concurrent readers share the winning instance,"
1351+
"var overrides = global::System.Threading.Volatile.Read(ref __metatableOverrides);"
13801352
);
13811353
builder.AppendLine(
1382-
"// including a custom metatable installed while initialization was in progress."
1354+
"return overrides != null && overrides.TryGetValue(this, out var entry) ? entry.Value : __metatable;"
13831355
);
1356+
}
1357+
builder.AppendLine("set");
1358+
using (builder.BeginBlockScope())
1359+
{
13841360
builder.AppendLine(
1385-
"return global::System.Threading.Interlocked.CompareExchange(ref __metatable, metatable, null) ?? metatable;"
1361+
"__GetMetatableOverrides().GetValue(this, _ => new __MetatableOverride(value)).Value = value;"
13861362
);
13871363
}
1364+
}
13881365

1389-
builder.AppendLine("set");
1390-
using (builder.BeginBlockScope())
1366+
builder.AppendLine("static global::Lua.LuaTable __CreateMetatable()");
1367+
using (builder.BeginBlockScope())
1368+
{
1369+
builder.AppendLine("var metatable = new global::Lua.LuaTable();");
1370+
foreach (var metamethod in metamethods)
13911371
{
1372+
var metaMethodName = metamethod.ToString();
1373+
var lowerName = metaMethodName.ToLower();
13921374
builder.AppendLine(
1393-
"global::System.Threading.Volatile.Write(ref __metatable, value);"
1375+
$"metatable[global::Lua.Runtime.Metamethods.{metaMethodName}] = __metamethod_{lowerName};"
13941376
);
13951377
}
1378+
builder.AppendLine("metatable.Freeze();");
1379+
builder.AppendLine("return metatable;");
13961380
}
13971381

1398-
builder.AppendLine("static global::Lua.LuaTable? __metatable;");
1382+
builder.AppendLine("sealed class __MetatableOverride");
1383+
using (builder.BeginBlockScope())
1384+
{
1385+
builder.AppendLine("public volatile global::Lua.LuaTable? Value;");
1386+
builder.AppendLine(
1387+
"public __MetatableOverride(global::Lua.LuaTable? value) => Value = value;"
1388+
);
1389+
}
1390+
builder.AppendLine(
1391+
"static global::System.Runtime.CompilerServices.ConditionalWeakTable<object, __MetatableOverride>? __metatableOverrides;"
1392+
);
1393+
builder.AppendLine(
1394+
"static global::System.Runtime.CompilerServices.ConditionalWeakTable<object, __MetatableOverride> __GetMetatableOverrides()"
1395+
);
1396+
using (builder.BeginBlockScope())
1397+
{
1398+
builder.AppendLine(
1399+
"var overrides = global::System.Threading.Volatile.Read(ref __metatableOverrides);"
1400+
);
1401+
builder.AppendLine("if (overrides != null) return overrides;");
1402+
builder.AppendLine("overrides = new();");
1403+
builder.AppendLine(
1404+
"return global::System.Threading.Interlocked.CompareExchange(ref __metatableOverrides, overrides, null) ?? overrides;"
1405+
);
1406+
}
1407+
builder.AppendLine(
1408+
"static readonly global::Lua.LuaTable __metatable = __CreateMetatable();"
1409+
);
13991410
builder.AppendLine();
1400-
1401-
return true;
14021411
}
14031412
}

0 commit comments

Comments
 (0)