From db533dedc3370f3d640a80cd1687fc875da81ceb Mon Sep 17 00:00:00 2001 From: Chase Cooper Date: Thu, 4 Jun 2026 23:52:09 -0400 Subject: [PATCH] Fast-path UserData member access via ILuaUserData.TryIndex. #234 --- .../LuaObjectGenerator.Emit.cs | 123 ++++++++++++++++++ src/Lua/LuaUserData.cs | 1 + src/Lua/Runtime/LuaVirtualMachine.cs | 43 ++++++ src/Lua/Standard/FileHandle.cs | 18 +++ 4 files changed, 185 insertions(+) diff --git a/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs b/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs index 02f39a1f..66532c49 100644 --- a/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs +++ b/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs @@ -178,6 +178,11 @@ TempCollections tempCollections return false; } + if (!TryEmitTryIndex(typeMetadata, builder, references, compilation)) + { + return false; + } + // implicit operator builder.AppendLine( $"public static implicit operator global::Lua.LuaValue({typeMetadata.FullTypeName} value)" @@ -1212,4 +1217,122 @@ in SourceProductionContext context return true; } + + static bool TryEmitTryIndex( + TypeMetadata typeMetadata, + CodeBuilder builder, + SymbolReferences references, + Compilation compilation + ) + { + builder.AppendLine("bool global::Lua.ILuaUserData.TryIndex(global::Lua.LuaValue key, ref global::Lua.LuaValue value, bool isGet)"); + using (builder.BeginBlockScope()) + { + builder.AppendLine("if (!key.TryRead(out var stringKey)) return false;"); + builder.AppendLine($"var userData = ({typeMetadata.FullTypeName})this;"); + builder.AppendLine(); + + builder.AppendLine("if (isGet)"); + using (builder.BeginBlockScope()) + { + foreach (var propertyMetadata in typeMetadata.Properties) + { + if (propertyMetadata.IsWriteOnly) + { + continue; + } + + if (SymbolEqualityComparer.Default.Equals(propertyMetadata.Type, references.LuaValue)) + { + if (propertyMetadata.IsStatic) + { + builder.AppendLine( + @$"if (stringKey == ""{propertyMetadata.LuaMemberName}"") {{ value = {typeMetadata.FullTypeName}.{propertyMetadata.Symbol.Name}; return true; }}" + ); + } + else + { + builder.AppendLine( + @$"if (stringKey == ""{propertyMetadata.LuaMemberName}"") {{ value = userData.{propertyMetadata.Symbol.Name}; return true; }}" + ); + } + } + else + { + var luaValuePrefix = GetLuaValuePrefix(propertyMetadata.Type, references, compilation); + if (propertyMetadata.IsStatic) + { + builder.AppendLine( + @$"if (stringKey == ""{propertyMetadata.LuaMemberName}"") {{ value = {luaValuePrefix}{typeMetadata.FullTypeName}.{propertyMetadata.Symbol.Name}); return true; }}" + ); + } + else + { + builder.AppendLine( + @$"if (stringKey == ""{propertyMetadata.LuaMemberName}"") {{ value = {luaValuePrefix}userData.{propertyMetadata.Symbol.Name}); return true; }}" + ); + } + } + } + + foreach ( + var methodMetadata in typeMetadata.Methods.Where(x => x.HasMemberAttribute) + ) + { + builder.AppendLine( + @$"if (stringKey == ""{methodMetadata.LuaMemberName}"") {{ value = new global::Lua.LuaValue(__function_{methodMetadata.LuaMemberName}); return true; }}" + ); + } + } + + builder.AppendLine("else"); + using (builder.BeginBlockScope()) + { + foreach (var propertyMetadata in typeMetadata.Properties) + { + if (propertyMetadata.IsReadOnly) + { + continue; + } + + var readExpr = GetLuaValueReadExpression( + propertyMetadata.Type, + references, + compilation + ); + if (propertyMetadata.IsStatic) + { + builder.AppendLine( + @$"if (stringKey == ""{propertyMetadata.LuaMemberName}"") {{ {typeMetadata.FullTypeName}.{propertyMetadata.Symbol.Name} = {readExpr}; return true; }}" + ); + } + else + { + builder.AppendLine( + @$"if (stringKey == ""{propertyMetadata.LuaMemberName}"") {{ userData.{propertyMetadata.Symbol.Name} = {readExpr}; return true; }}" + ); + } + } + } + + builder.AppendLine("return false;"); + } + builder.AppendLine(); + + return true; + } + + static string GetLuaValueReadExpression(ITypeSymbol type, SymbolReferences references, Compilation compilation) + { + if (SymbolEqualityComparer.Default.Equals(type, references.LuaValue)) + return "value"; + var typeName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + if (SymbolEqualityComparer.Default.Equals(type, references.String)) + return $"value.Read<{typeName}>()"; + if (SymbolEqualityComparer.Default.Equals(type, references.Double)) + return $"value.Read<{typeName}>()"; + if (SymbolEqualityComparer.Default.Equals(type, references.Boolean)) + return $"value.Read<{typeName}>()"; + return $"value.TryRead<{typeName}>(out var __readResult) ? __readResult : throw new global::System.InvalidOperationException($\"Cannot convert LuaValue type '{{value.Type}}' to '{typeName}'\")"; + } } diff --git a/src/Lua/LuaUserData.cs b/src/Lua/LuaUserData.cs index 8ca3fba0..7d6a0cb2 100644 --- a/src/Lua/LuaUserData.cs +++ b/src/Lua/LuaUserData.cs @@ -20,4 +20,5 @@ public interface ILuaUserData // We use span for compatibility with lua5.4. Span UserValues => default; + bool TryIndex(LuaValue key, ref LuaValue value, bool isGet) => false; } diff --git a/src/Lua/Runtime/LuaVirtualMachine.cs b/src/Lua/Runtime/LuaVirtualMachine.cs index 49a13c60..f65507d8 100644 --- a/src/Lua/Runtime/LuaVirtualMachine.cs +++ b/src/Lua/Runtime/LuaVirtualMachine.cs @@ -1866,6 +1866,18 @@ out bool doRestart const int MAX_LOOP = 100; doRestart = false; var skip = targetTable.Type == LuaValueType.Table; + + if (targetTable.Type == LuaValueType.UserData) + { + var userData = targetTable.UnsafeRead(); + var result = default(LuaValue); + if (userData.TryIndex(key, ref result, true)) + { + value = result; + return true; + } + } + for (var i = 0; i < MAX_LOOP; i++) { if (table.TryReadTable(out var luaTable)) @@ -2027,6 +2039,17 @@ CancellationToken ct var targetTable = table; const int MAX_LOOP = 100; var skip = targetTable.Type == LuaValueType.Table; + + if (targetTable.Type == LuaValueType.UserData) + { + var userData = targetTable.UnsafeRead(); + var result = default(LuaValue); + if (userData.TryIndex(key, ref result, true)) + { + return new(result); + } + } + for (var i = 0; i < MAX_LOOP; i++) { if (table.TryReadTable(out var luaTable)) @@ -2124,6 +2147,16 @@ out bool doRestart const int MAX_LOOP = 100; doRestart = false; var skip = targetTable.Type == LuaValueType.Table; + + if (targetTable.Type == LuaValueType.UserData) + { + var userData = targetTable.UnsafeRead(); + if (userData.TryIndex(key, ref value, false)) + { + return true; + } + } + for (var i = 0; i < MAX_LOOP; i++) { if (table.TryReadTable(out var luaTable)) @@ -2279,6 +2312,16 @@ CancellationToken ct var targetTable = table; const int MAX_LOOP = 100; var skip = targetTable.Type == LuaValueType.Table; + + if (targetTable.Type == LuaValueType.UserData) + { + var userData = targetTable.UnsafeRead(); + if (userData.TryIndex(key, ref value, false)) + { + return default; + } + } + for (var i = 0; i < MAX_LOOP; i++) { if (table.TryReadTable(out var luaTable)) diff --git a/src/Lua/Standard/FileHandle.cs b/src/Lua/Standard/FileHandle.cs index 3fc11ce2..aca00e48 100644 --- a/src/Lua/Standard/FileHandle.cs +++ b/src/Lua/Standard/FileHandle.cs @@ -49,6 +49,24 @@ public sealed class FileHandle : ILuaUserData set => fileHandleMetatable = value; } + bool ILuaUserData.TryIndex(LuaValue key, ref LuaValue value, bool isGet) + { + if (!isGet) return false; + if (!key.TryRead(out var name)) return false; + value = name switch + { + "close" => new LuaValue(CloseFunction), + "flush" => new LuaValue(FlushFunction), + "lines" => new LuaValue(LinesFunction), + "read" => new LuaValue(ReadFunction), + "seek" => new LuaValue(SeekFunction), + "setvbuf" => new LuaValue(SetVBufFunction), + "write" => new LuaValue(WriteFunction), + _ => default, + }; + return value.Type != LuaValueType.Nil; + } + static LuaTable? fileHandleMetatable; static FileHandle()