Skip to content

Fix concurrent publication of generated LuaObject metatables - #348

Merged
akeit0 merged 1 commit into
mainfrom
codex/issue309-metatable-publication
Oct 10, 2026
Merged

akeit0 merged 1 commit into
mainfrom
codex/issue309-metatable-publication

Conversation

@akeit0

@akeit0 akeit0 commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Concurrent first access to generated [LuaObject] metatables can expose an incomplete table even when callers use separate LuaState and object instances. The getter currently publishes the table before adding its metamethods, and competing initializers also mutate the shared field's current table.

Build each candidate locally, then publish it with Interlocked.CompareExchange after all metamethods are installed. Use Volatile.Read/Write for the shared property's access. Readers converge on the published instance, and initialization does not overwrite an existing non-null custom metatable. Explicit replacement and null-triggered reinitialization retain their existing behavior. Generated comments explain why publication occurs last.

Fixes #309.

Validation:

  • Reproduced the original failure with eight threads, separate states/objects, and 500 synchronized initialization rounds. The regression checks all six generated metamethods, shared table identity, property writes, and Lua method/metamethod calls.
  • Added replacement/reset coverage. Both new tests pass with the fix; the concurrent regression fails against main with missing metamethods and invalid userdata operations.
  • Release runtime tests: 322 passed, excluding ExpectedFailure, matching CI. Source Generator tests: 1 passed.
  • GitHub Actions lint and test checks passed for commit 816a8a6.
  • Runtime builds and generated consumers (class, generic class, record) compile for netstandard2.1, net6.0, net8.0, and net10.0 with no warnings/errors. Formatting and diff checks pass.

Performance measured with BenchmarkDotNet 0.14.0, Windows x64/.NET 8.0.31, Release, two launches, five warmups, twelve measured iterations per launch. Both versions use the same main runtime; only the generator differs. The Lua workload performs 100,000 iterations of property reads and method calls and checks its result.

Workload main mean PR mean Allocation per operation (both)
Cached metatable getter 1.305 ns 1.051 ns 0 B
Reset and initialize 49.252 ns 49.210 ns 848 B
Lua member access 15.345 ms 14.620 ms 70 B

No regression was observed in these measurements; the Lua workload took approximately 4.7% less time. Both cached-getter measurements have bimodal distributions, so their small absolute difference should not be treated as a reliable speedup. Unity Mono/IL2CPP execution was not measured.

The initialized getter takes no lock. Concurrent cold access can allocate losing candidate tables, which are discarded. This fix makes initial construction/publication safe; it does not synchronize subsequent mutations of a shared metatable or concurrent use of one LuaState.

Temporary benchmark sources, consumer projects, and logs remain outside the repository. This PR is left unmerged for the maintainer's final decision.

@akeit0
akeit0 merged commit c694d1b into main Oct 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Thread safety for [LuaObject] lazy initialization of Metatable property

1 participant