-
Notifications
You must be signed in to change notification settings - Fork 33
version 0.5 #135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
version 0.5 #135
Conversation
@nuskey8 |
I'm currently working on another project, so I'll review it soon. One thing is that the Lua.CodeAnalysis API has been removed, but I would like to keep it because it's simply useful for analyzing Lua syntax trees. |
FYI current 0.5 dev branch doesn't build due to missing |
Thank you!! Unfortunately, I was able to build it in my environment.... |
Thanks, got the update, I can build, but if I open my project in Unity 6 with the new DLL I get this error:
I cannot add that assembly to my asmdef, it's not in the list. I found another Unity project that references TimeProvider, and I updated my asmdef Apparently that project includes the DLL, which is currently missing in my project. I suspect this means adding the TimeProvider.dll to the distribution / build output. Okay, fixed it by adding the TimeProvider.dll and updating the asmdefs with "override references" flag turned on and having both Lua and TimeProvider in this list: |
I'm a little confused with Is there a reason for this or does this simply need some more work to achieve consistency? |
This is a technique to slightly reduce allocations with respect to await. ... But I'm not sure it makes that much sense. |
change: loaded module returns better chunk name
@nuskey8 This is not perfect, but it helps to know the changes. Lua-CSharp v0.5 API Change generated by Claude CodeLua-CSharp v0.5 API ChangesThis document outlines the API changes between the main branch and v0.5-dev. Version 0.5 represents a major architectural shift with significant breaking changes focused on platform abstraction, security, and improved async support. Breaking Changes1. LuaState Constructor and Platform SystemThe static factory method now accepts an optional platform configuration: // Before (main branch)
var state = LuaState.Create();
// After (v0.5)
var state = LuaState.Create(); // Uses LuaPlatform.Default
var state = LuaState.Create(customPlatform); New Platform Properties in LuaState: public LuaPlatform Platform { get; }
public ILuaFileSystem FileSystem => Platform.FileSystem ?? throw new InvalidOperationException(...);
public ILuaOsEnvironment OsEnvironment => Platform.OsEnvironment ?? throw new InvalidOperationException(...);
public TimeProvider TimeProvider => Platform.TimeProvider ?? throw new InvalidOperationException(...);
public ILuaStandardIO StandardIO => Platform.StandardIO; 2. Function Signature ChangesAll Lua function signatures have changed - the buffer parameter was removed: // Before (main branch)
async ValueTask<int> MyFunction(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken ct)
{
buffer.Span[0] = LuaValue.True;
return 1;
}
// After (v0.5)
async ValueTask<int> MyFunction(LuaFunctionExecutionContext context, CancellationToken ct)
{
return context.Return(LuaValue.True);
} 3. Thread Access SystemDirect thread access replaced with versioned // Before (main branch) - these properties/methods no longer exist
state.CurrentThread.Stack.Push(value);
state.Push(value);
// After (v0.5)
var access = state.RootAccess;
access.Push(value); Key Properties: public LuaThreadAccess RootAccess => new(mainThread, 0); 4. Module Storage ChangesModule tables moved to registry with standard Lua keys: // Before (main branch)
public LuaTable LoadedModules => packages; // Direct field
// After (v0.5)
internal const string LoadedKeyForRegistry = "_LOADED";
internal const string PreloadKeyForRegistry = "_PRELOAD";
public LuaTable LoadedModules => registry[LoadedKeyForRegistry].Read<LuaTable>();
public LuaTable PreloadModules => registry[PreloadKeyForRegistry].Read<LuaTable>(); New FeaturesPlatform Abstraction LayerNew public class LuaPlatform
{
public static LuaPlatform Default { get; } = new()
{
FileSystem = new FileSystem(),
OsEnvironment = new SystemOsEnvironment(),
StandardIO = new ConsoleStandardIO(),
TimeProvider = TimeProvider.System
};
public ILuaFileSystem? FileSystem { get; init; }
public ILuaOsEnvironment? OsEnvironment { get; init; }
public ILuaStandardIO StandardIO { get; init; } = new ConsoleStandardIO();
public TimeProvider? TimeProvider { get; init; }
} ILuaStream InterfaceNew unified stream abstraction: public interface ILuaStream : IDisposable
{
bool IsOpen { get; }
LuaFileOpenMode Mode { get; }
ValueTask<string> ReadAllAsync(CancellationToken cancellationToken);
ValueTask<double?> ReadNumberAsync(CancellationToken cancellationToken);
ValueTask<string?> ReadLineAsync(bool keepEol, CancellationToken cancellationToken);
ValueTask<string?> ReadAsync(int count, CancellationToken cancellationToken);
ValueTask WriteAsync(ReadOnlyMemory<char> content, CancellationToken cancellationToken);
ValueTask FlushAsync(CancellationToken cancellationToken);
ValueTask CloseAsync(CancellationToken cancellationToken);
long Seek(SeekOrigin origin, long offset);
void SetVBuf(LuaFileBufferingMode mode, int size);
} File System AbstractionComplete file system abstraction for sandboxing: public interface ILuaFileSystem
{
ValueTask<ILuaStream> OpenAsync(string filename, LuaFileOpenMode mode, CancellationToken cancellationToken);
ValueTask<ILuaStream> OpenTempFileAsync(CancellationToken cancellationToken);
ValueTask RemoveAsync(string filename, CancellationToken cancellationToken);
ValueTask RenameAsync(string oldName, string newName, CancellationToken cancellationToken);
string GetFullPath(string path);
} OS Environment InterfaceOperating system operations are now abstracted: public interface ILuaOsEnvironment
{
string? GetEnvironmentVariable(string name);
ValueTask Exit(int exitCode, CancellationToken cancellationToken);
double GetTotalProcessorTime();
} LuaThreadAccess APICore Concept
Version Tracking Purpose:
Call Stack Pop Version Changes:
The version is restored after normal function returns, so public readonly struct LuaThreadAccess
{
public readonly LuaThread Thread;
public readonly int Version;
public bool IsValid => Version == Thread.CurrentVersion;
public void ThrowIfInvalid() { /* throws if invalid */ }
} Version Lifecycle Example: var access1 = thread.CurrentAccess; // Version = 1
// Some operation that changes thread state (e.g., function call)
var access2 = thread.CurrentAccess; // Version = 2
// access1.IsValid == false (version mismatch)
// access2.IsValid == true (current version) Call Stack Version Change Example: // Normal function call - version is restored
var myFunc = new LuaFunction("test", async (context, ct) =>
{
var accessBefore = context.Access; // Version = N
// Call another function
await accessBefore.Call(anotherFunc, []); // Version = N + 1
// After normal return, version is restored
accessBefore.ThrowIfInvalid(); // Still valid! Version restored to N
return context.Return(LuaValue.True);
}); Getting Access// From LuaState - always valid with version 0
var access = state.RootAccess;
// From LuaThread - current version
var access = thread.CurrentAccess;
// From LuaFunctionExecutionContext
var access = context.Access; Extension MethodsAll implemented in Script Execution// Execute Lua source code
ValueTask<LuaValue[]> DoStringAsync(string source, string? chunkName = null, CancellationToken ct = default);
ValueTask<int> DoStringAsync(string source, Memory<LuaValue> buffer, string? chunkName = null, CancellationToken ct = default);
// Execute bytecode
ValueTask<LuaValue[]> DoBytesAsync(ReadOnlySpan<byte> source, string chunkName, CancellationToken ct = default);
ValueTask<int> DoBytesAsync(ReadOnlySpan<byte> source, Memory<LuaValue> buffer, string chunkName, CancellationToken ct = default);
// Execute file
ValueTask<LuaValue[]> DoFileAsync(string path, CancellationToken ct = default);
ValueTask<int> DoFileAsync(string path, Memory<LuaValue> buffer, CancellationToken ct = default); Stack Operationsvoid Push(LuaValue value);
void Push(params ReadOnlySpan<LuaValue> span);
LuaValue Pop();
void Pop(int count);
LuaReturnValuesReader ReadReturnValues(int argumentCount); Arithmetic OperationsValueTask<LuaValue> Add(LuaValue x, LuaValue y, CancellationToken ct = default);
ValueTask<LuaValue> Sub(LuaValue x, LuaValue y, CancellationToken ct = default);
ValueTask<LuaValue> Mul(LuaValue x, LuaValue y, CancellationToken ct = default);
ValueTask<LuaValue> Div(LuaValue x, LuaValue y, CancellationToken ct = default);
ValueTask<LuaValue> Mod(LuaValue x, LuaValue y, CancellationToken ct = default);
ValueTask<LuaValue> Pow(LuaValue x, LuaValue y, CancellationToken ct = default);
ValueTask<LuaValue> Unm(LuaValue value, CancellationToken ct = default);
ValueTask<LuaValue> Len(LuaValue value, CancellationToken ct = default); Comparison OperationsValueTask<bool> LessThan(LuaValue x, LuaValue y, CancellationToken ct = default);
ValueTask<bool> LessThanOrEquals(LuaValue x, LuaValue y, CancellationToken ct = default);
ValueTask<bool> Equals(LuaValue x, LuaValue y, CancellationToken ct = default); Table OperationsValueTask<LuaValue> GetTable(LuaValue table, LuaValue key, CancellationToken ct = default);
ValueTask SetTable(LuaValue table, LuaValue key, LuaValue value, CancellationToken ct = default); Function CallsValueTask<LuaValue[]> Call(LuaValue function, ReadOnlySpan<LuaValue> arguments, CancellationToken ct = default);
ValueTask<int> Call(int funcIndex, int returnBase, CancellationToken ct = default); Compilation System ChangesChunk to Prototype RenameThe // Old (main branch)
public sealed class Chunk
{
public required string Name { get; init; }
public required Instruction[] Instructions { get; init; }
// ... more properties
}
// New (v0.5) - primary constructor
public sealed class Prototype(
string chunkName,
int lineDefined,
int lastLineDefined,
int parameterCount,
int maxStackSize,
bool hasVariableArguments,
LuaValue[] constants,
Instruction[] code,
Prototype[] childPrototypes,
int[] lineInfo,
LocalVariable[] localVariables,
UpValueDesc[] upValues
) Rename
LuaCompiler APINew static API for compilation: public static class LuaCompiler
{
public static ReadOnlySpan<byte> LuaByteCodeSignature => Header.LuaSignature;
public static Prototype UnDump(ReadOnlySpan<byte> span, ReadOnlySpan<char> name);
public static byte[] Dump(Prototype prototype, bool useLittleEndian = true);
} LuaState Load MethodsLoad methods in LuaState for creating closures: // Load from string
public LuaClosure Load(ReadOnlySpan<char> chunk, string chunkName, LuaTable? environment = null);
// Load from bytes (auto-detects source vs bytecode)
public LuaClosure Load(ReadOnlySpan<byte> chunk, string? chunkName = null, string mode = "bt", LuaTable? environment = null); LuaStateExtensionsConvenience methods delegating to RootAccess: // All delegate to state.RootAccess.Method(...)
public static ValueTask<LuaValue[]> DoStringAsync(this LuaState state, string source, string? chunkName = null, CancellationToken ct = default);
public static ValueTask<int> DoStringAsync(this LuaState state, string source, Memory<LuaValue> buffer, string? chunkName = null, CancellationToken ct = default);
public static ValueTask<LuaValue[]> DoBytesAsync(this LuaState state, ReadOnlySpan<byte> source, string chunkName, CancellationToken ct = default);
public static ValueTask<LuaValue[]> DoFileAsync(this LuaState state, string path, CancellationToken ct = default);
public static ValueTask<LuaClosure> LoadFileAsync(this LuaState state, string fileName, string mode, LuaTable? environment, CancellationToken ct); Module System ChangesILuaModuleLoader InterfaceEnhanced with existence checking: public interface ILuaModuleLoader
{
bool Exists(string moduleName);
ValueTask<LuaModule> LoadAsync(string moduleName, CancellationToken cancellationToken);
} LuaModule TypesSupport for both text and binary modules: public enum LuaModuleType
{
Text, // Lua source code
Bytes // Compiled bytecode
}
public class LuaModule
{
public required string Name { get; init; }
public required LuaModuleType Type { get; init; }
public string? Text { get; init; }
public byte[]? Bytes { get; init; }
public string ReadText() => Text ?? throw new InvalidOperationException("Module is not text-based");
public ReadOnlySpan<byte> ReadBytes() => Bytes ?? throw new InvalidOperationException("Module is not bytes-based");
} ModuleLibrary ChangesEnhanced require function with preload support: public sealed class ModuleLibrary
{
internal const string LoadedKeyForRegistry = "_LOADED";
internal const string PreloadKeyForRegistry = "_PRELOAD";
public readonly LuaFunction RequireFunction;
public readonly LuaFunction SearchPathFunction; // New in v0.5
} Lua.Unity Package ChangesBytecode Asset SupportNew asset types for precompiled Lua: // Base class for all Lua assets
public abstract class LuaAssetBase : ScriptableObject
{
public abstract LuaModule GetModule(string searchedName);
protected string GetChunkName(string searchedName)
{
#if UNITY_EDITOR
return $"@{AssetDatabase.GetAssetPath(this)}";
#else
return $"@{searchedName}.lua";
#endif
}
}
// For source code (.lua files)
public class LuaAsset : LuaAssetBase
{
[SerializeField] private string text;
public string Text => text;
public override LuaModule GetModule(string searchedName) => new()
{
Name = GetChunkName(searchedName),
Type = LuaModuleType.Text,
Text = text
};
}
// For bytecode (.luac files)
public class LuacAsset : LuaAssetBase
{
[SerializeField] private byte[] bytes;
public ReadOnlySpan<byte> Bytes => bytes;
public override LuaModule GetModule(string searchedName) => new()
{
Name = GetChunkName(searchedName),
Type = LuaModuleType.Bytes,
Bytes = bytes
};
} Unity Platform IntegrationUnity-specific platform implementations: UnityApplicationOsEnvironment: public class UnityApplicationOsEnvironment : ILuaOsEnvironment
{
public UnityApplicationOsEnvironment(Dictionary<string, string> environmentVariables = null, bool allowToQuitOnExitCall = false)
{
EnvironmentVariables = environmentVariables ?? new Dictionary<string, string>();
AllowToQuitOnExitCall = allowToQuitOnExitCall;
}
public bool AllowToQuitOnExitCall { get; }
public Dictionary<string, string> EnvironmentVariables { get; }
public string GetEnvironmentVariable(string name)
{
if (EnvironmentVariables.TryGetValue(name, out var value))
{
return value;
}
return null;
}
public ValueTask Exit(int exitCode, CancellationToken cancellationToken)
{
if (AllowToQuitOnExitCall)
{
Application.Quit(exitCode);
throw new OperationCanceledException();
}
else
{
throw new InvalidOperationException("Application exit is not allowed in this environment.");
}
}
public double GetTotalProcessorTime() => Time.time;
} UnityStandardIO: public class UnityStandardIO : ILuaStandardIO
{
public UnityStandardIO(ILuaStream input = null)
{
if (input != null)
{
Input = input;
}
else
{
Input = new DummyInputStream();
}
}
public ILuaStream Input { get; }
public ILuaStream Output { get; } = new DebugLogStream(false);
public ILuaStream Error { get; } = new DebugLogStream(true);
} DummyInputStream (for simulated console input): public class DummyInputStream : ILuaStream
{
public bool IsOpen { get; } = true;
public LuaFileOpenMode Mode => LuaFileOpenMode.Read;
public void Dispose() { }
} DebugLogStream (outputs to Unity Debug.Log): public class DebugLogStream : ILuaStream
{
public DebugLogStream(bool isError = false)
{
IsError = isError;
}
public bool IsError { get; } = false;
public bool IsOpen { get; } = true;
public LuaFileOpenMode Mode => LuaFileOpenMode.Write;
private readonly StringBuilder stringBuilder = new();
ValueTask ILuaStream.WriteAsync(ReadOnlyMemory<char> content, CancellationToken cancellationToken)
{
stringBuilder.Append(content.Span);
return default;
}
ValueTask ILuaStream.FlushAsync(CancellationToken cancellationToken)
{
if (stringBuilder.Length > 0)
{
var message = stringBuilder.ToString();
if (IsError)
UnityEngine.Debug.LogError(message);
else
UnityEngine.Debug.Log(message);
stringBuilder.Clear();
}
return default;
}
public ValueTask Close(CancellationToken cancellationToken)
{
throw new NotSupportedException("DebugLogStream cannot be closed.");
}
public void Dispose() { }
} Enhanced Module LoadersUpdated to support polymorphic assets: public class AddressablesModuleLoader : ILuaModuleLoader
{
private readonly Dictionary<string, LuaAssetBase> cache = new();
public bool Exists(string moduleName) =>
Addressables.LoadResourceLocationsAsync(moduleName, typeof(LuaAssetBase)).Result.Count > 0;
public async ValueTask<LuaModule> LoadAsync(string moduleName, CancellationToken cancellationToken)
{
if (!cache.TryGetValue(moduleName, out var asset))
{
asset = await Addressables.LoadAssetAsync<LuaAssetBase>(moduleName);
cache[moduleName] = asset;
}
return asset.GetModule(moduleName);
}
} Migration Guide1. Update LuaState Creation// Basic migration - no changes needed
var state = LuaState.Create();
// Custom platform
var platform = new LuaPlatform
{
FileSystem = myFileSystem,
OsEnvironment = myOsEnvironment,
StandardIO = myStandardIO,
TimeProvider = TimeProvider.System
};
var state = LuaState.Create(platform); 2. Update Function Signatures// Before
async ValueTask<int> MyFunction(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken ct)
{
buffer.Span[0] = LuaValue.True;
return 1;
}
// After
async ValueTask<int> MyFunction(LuaFunctionExecutionContext context, CancellationToken ct)
{
return context.Return(LuaValue.True);
} 3. Update Thread Access// Before
state.CurrentThread.Stack.Push(value);
state.Push(value);
// After
state.RootAccess.Push(value); 4. Update Chunk References// Before
Chunk chunk = compiler.Compile(source);
var closure = new LuaClosure(state, chunk);
// After
var closure = state.Load(source, chunkName);
// Or use LuaCompiler for bytecode operations
var prototype = LuaCompiler.UnDump(bytecode, name);
var closure = new LuaClosure(state, prototype); 5. Update Module Loaders// Before
public class MyLoader : ILuaModuleLoader
{
public async ValueTask<LuaModule> LoadAsync(string name, CancellationToken ct) { /* ... */ }
}
// After - add Exists method
public class MyLoader : ILuaModuleLoader
{
public bool Exists(string moduleName) => File.Exists($"{moduleName}.lua");
public async ValueTask<LuaModule> LoadAsync(string name, CancellationToken ct) { /* ... */ }
} Key Benefits
SummaryVersion 0.5 represents a major architectural evolution focusing on:
While the breaking changes require migration effort, they enable Lua-CSharp to be safely and efficiently embedded in production environments requiring script sandboxing and security. Debug Library (New in v0.5)OverviewVersion 0.5 introduces a comprehensive debug library ( Debug Library FunctionsThe debug library includes all standard Lua 5.2 debug functions: public class DebugLibrary
{
public readonly LibraryFunction[] Functions =
[
new("debug", "getlocal", GetLocal),
new("debug", "setlocal", SetLocal),
new("debug", "getupvalue", GetUpValue),
new("debug", "setupvalue", SetUpValue),
new("debug", "getmetatable", GetMetatable),
new("debug", "setmetatable", SetMetatable),
new("debug", "getuservalue", GetUserValue),
new("debug", "setuservalue", SetUserValue),
new("debug", "traceback", Traceback),
new("debug", "getregistry", GetRegistry),
new("debug", "upvalueid", UpValueId),
new("debug", "upvaluejoin", UpValueJoin),
new("debug", "gethook", GetHook),
new("debug", "sethook", SetHook),
new("debug", "getinfo", GetInfo),
];
} Key Debug FunctionsLocal Variable Inspection-- Get local variable by index from stack level
local name, value = debug.getlocal(level, local_index)
-- Set local variable by index at stack level
local name = debug.setlocal(level, local_index, new_value) UpValue Manipulation-- Get upvalue from function
local name, value = debug.getupvalue(func, upvalue_index)
-- Set upvalue in function
local name = debug.setupvalue(func, upvalue_index, new_value)
-- Get unique identifier for upvalue
local id = debug.upvalueid(func, upvalue_index)
-- Share upvalue between functions
debug.upvaluejoin(func1, index1, func2, index2) Function Information-- Get detailed function information
local info = debug.getinfo(func_or_level, "what_string")
-- Returns table with: source, short_src, linedefined, lastlinedefined, what, name, namewhat, nups, nparams, isvararg, activelines, func Stack Tracebacks-- Generate traceback string
local trace = debug.traceback([thread,] [message,] [level]) Hook System-- Set debug hooks for line, call, return events
debug.sethook([thread,] hook_function, mask, [count])
-- Get current hook information
local hook, mask, count = debug.gethook([thread]) Usage ExamplesExample 1: Stack Inspectionfunction inspect_stack()
local level = 1
while true do
local info = debug.getinfo(level, "Sln")
if not info then break end
print(string.format("Level %d: %s:%d in %s",
level, info.short_src, info.currentline, info.name or "?"))
-- Print local variables
local i = 1
while true do
local name, value = debug.getlocal(level, i)
if not name then break end
print(string.format(" Local %s = %s", name, tostring(value)))
i = i + 1
end
level = level + 1
end
end Example 2: Function Introspectionfunction analyze_function(func)
local info = debug.getinfo(func, "Snu")
print("Function info:")
print(" Source:", info.source)
print(" Parameters:", info.nparams)
print(" Upvalues:", info.nups)
print(" Variadic:", info.isvararg)
-- Print upvalues
for i = 1, info.nups do
local name, value = debug.getupvalue(func, i)
print(string.format(" Upvalue %d: %s = %s", i, name, tostring(value)))
end
end LightUserData (New in v0.5)OverviewVersion 0.5 introduces LuaValueType Enum Additionpublic enum LuaValueType : byte
{
Nil,
Boolean,
String,
Number,
Function,
Thread,
LightUserData, // New in v0.5
UserData,
Table,
} LightUserData vs UserData
Creating LightUserData// Internal constructor - used for C# objects
LuaValue(object obj)
{
Type = LuaValueType.LightUserData;
referenceValue = obj;
}
// Usage
var myObject = new MyClass();
var lightUserData = new LuaValue(myObject); // Creates LightUserData Usage ExamplesExample 1: Passing C# Objects to Luapublic class ConfigManager
{
public string Version { get; set; } = "1.0";
public bool DebugMode { get; set; } = false;
}
// In C#
var config = new ConfigManager();
var configValue = new LuaValue(config); // LightUserData
// Pass to Lua function
state.Environment["config"] = configValue; Example 2: Bridge Functions for LightUserData// Bridge function to access LightUserData properties
var getConfigProperty = new LuaFunction("get_config_property", (context, ct) =>
{
var config = context.GetArgument<ConfigManager>(0);
var property = context.GetArgument<string>(1);
var value = property switch
{
"version" => config.Version,
"debug" => config.DebugMode,
_ => throw new ArgumentException($"Unknown property: {property}")
};
return context.Return(LuaValue.FromObject(value));
});
state.Environment["get_config"] = getConfigProperty; CSharpClosure vs LuaClosure (Renamed in v0.5)OverviewVersion 0.5 introduces a clear distinction between C#-implemented functions ( Changes from v0.4.2Before (v0.4.2):// Single Closure class for both Lua and C# functions
public sealed class Closure : LuaFunction
{
Chunk proto; // For Lua functions
FastListCore<UpValue> upValues;
public Closure(LuaState state, Chunk proto, LuaTable? environment = null)
{
// Constructor for Lua functions
}
} After (v0.5):// Separate classes for different function types
// For C# functions with upvalues
public sealed class CSharpClosure(
string name,
LuaValue[] upValues,
Func<LuaFunctionExecutionContext, CancellationToken, ValueTask<int>> func
) : LuaFunction(name, func)
{
public readonly LuaValue[] UpValues = upValues;
}
// For compiled Lua functions
public sealed class LuaClosure : LuaFunction
{
FastListCore<UpValue> upValues;
public LuaClosure(LuaThread thread, Prototype proto, LuaTable? environment = null)
: base(proto.ChunkName, static (context, ct) => LuaVirtualMachine.ExecuteClosureAsync(context.Thread, ct))
{
Proto = proto;
// Setup upvalues...
}
} CSharpClosure FeaturesUpvalue Support// Create C# function with upvalues
var upvalues = new LuaValue[]
{
LuaValue.String("captured_string"),
LuaValue.Number(42)
};
var closure = new CSharpClosure("my_function", upvalues, async (context, ct) =>
{
// Access upvalues
var upvalues = context.GetCsClosure()!.UpValues;
var capturedString = upvalues[0].Read<string>();
var capturedNumber = upvalues[1].Read<double>();
return context.Return($"Captured: {capturedString}, {capturedNumber}");
}); Usage ExamplesExample 1: Creating CSharpClosure with Upvalues// Counter closure example
var createCounter = (int initial) =>
{
var upvalues = new LuaValue[] { LuaValue.Number(initial) };
return new CSharpClosure("counter", upvalues, (context, ct) =>
{
var upvalues = context.GetCsClosure()!.UpValues;
var currentValue = upvalues[0].Read<double>();
upvalues[0] = LuaValue.Number(currentValue + 1);
return context.Return(currentValue);
});
};
var counter = createCounter(0);
state.Environment["counter"] = counter; -- Usage in Lua
print(counter()) -- 0
print(counter()) -- 1
print(counter()) -- 2 Example 2: Distinguishing Function Typesvar inspectFunction = new LuaFunction("inspect_function", (context, ct) =>
{
var func = context.GetArgument<LuaFunction>(0);
var functionType = func switch
{
CSharpClosure csharp => $"C# closure with {csharp.UpValues.Length} upvalues",
LuaClosure lua => $"Lua closure from {lua.Proto.ChunkName}",
_ => "Other function type"
};
return context.Return(functionType);
}); LuaCoroutine API EnhancementsOverviewVersion 0.5 significantly enhances the Key Changes from v0.4.2Constructor and Lifecycle Management// Before (v0.4.2)
public LuaCoroutine(LuaFunction function, bool isProtectedMode)
{
IsProtectedMode = isProtectedMode;
Function = function;
buffer = ArrayPool<LuaValue>.Shared.Rent(1024); // Fixed buffer allocation
}
// After (v0.5)
public static LuaCoroutine Create(LuaThread parent, LuaFunction function, bool isProtectedMode)
{
if (!pool.TryPop(out LuaCoroutine result))
{
result = new();
}
result.Init(parent, function, isProtectedMode);
return result;
}
public void Release() // Explicit resource management
{
if (CoreData != null && CoreData.CallStack.Count != 0)
{
throw new InvalidOperationException("This thread is running! Call stack is not empty!!");
}
ReleaseCore();
pool.TryPush(this); // Return to pool
} Context Structures Optimization// Before (v0.4.2) - Array allocation
struct YieldContext
{
public required LuaValue[] Results; // Heap allocation
}
struct ResumeContext
{
public required LuaValue[] Results; // Heap allocation
}
// After (v0.5) - Span-based with stack reference
readonly struct YieldContext(LuaStack stack, int argCount)
{
public ReadOnlySpan<LuaValue> Results => stack.AsSpan()[^argCount..]; // Zero allocation
}
readonly struct ResumeContext(LuaStack? stack, int argCount)
{
public ReadOnlySpan<LuaValue> Results => stack!.AsSpan()[^argCount..]; // Zero allocation
public bool IsDead => stack == null; // Death detection
} New Features1. Object Pooling// LuaCoroutine now implements IPoolNode<LuaCoroutine>
public sealed class LuaCoroutine : LuaThread, IValueTaskSource<YieldContext>, IValueTaskSource<ResumeContext>, IPoolNode<LuaCoroutine>
{
static LinkedPool<LuaCoroutine> pool;
LuaCoroutine? nextNode;
ref LuaCoroutine? IPoolNode<LuaCoroutine>.NextNode => ref nextNode;
}
// Usage
var coroutine = LuaCoroutine.Create(thread, function, isProtected);
// ... use coroutine
coroutine.Release(); // Returns to pool for reuse 2. Enhanced Resume API// Multiple resume overloads for different scenarios
public ValueTask<int> ResumeAsync(LuaStack stack, CancellationToken cancellationToken = default)
{
return ResumeAsyncCore(stack, stack.Count, 0, null, cancellationToken);
}
public override ValueTask<int> ResumeAsync(LuaFunctionExecutionContext context, CancellationToken cancellationToken = default)
{
return ResumeAsyncCore(context.Thread.Stack, context.ArgumentCount, context.ReturnFrameBase, context.Thread, cancellationToken);
} 3. Enhanced Yield API// Multiple yield overloads for different scenarios
public ValueTask<int> YieldAsync(LuaStack stack, CancellationToken cancellationToken = default)
{
return YieldAsyncCore(stack, stack.Count, 0, null, cancellationToken);
}
public override ValueTask<int> YieldAsync(LuaFunctionExecutionContext context, CancellationToken cancellationToken = default)
{
return YieldAsyncCore(context.Thread.Stack, context.ArgumentCount, context.ReturnFrameBase, context.Thread, cancellationToken);
} 4. Better State Management// New properties for coroutine management
public bool CanResume => status == (byte)LuaThreadStatus.Suspended;
public bool IsProtectedMode { get; private set; }
public LuaFunction Function { get; private set; } = null!;
internal Traceback? LuaTraceback => traceback; // Error tracking 5. Improved Error Handling// v0.5 includes comprehensive error handling with traceback preservation
catch (Exception ex) when (ex is not OperationCanceledException)
{
if (IsProtectedMode)
{
if (ex is ILuaTracebackBuildable tracebackBuildable)
{
traceback = tracebackBuildable.BuildOrGet(); // Preserve traceback
}
Volatile.Write(ref status, (byte)LuaThreadStatus.Dead);
ReleaseCore();
stack.PopUntil(returnBase);
stack.Push(false);
stack.Push(ex is LuaRuntimeException luaEx ? luaEx.ErrorObject : ex.Message);
return 2;
}
else
{
throw;
}
} 6. Advanced Memory Management// v0.5 eliminates fixed buffer allocation in favor of stack-based operations
internal void Init(LuaThread parent, LuaFunction function, bool isProtectedMode)
{
CoreData = ThreadCoreData.Create(); // Pooled core data
State = parent.State;
IsProtectedMode = isProtectedMode;
Function = function;
// No buffer allocation - uses stack directly
}
void ReleaseCore()
{
CoreData?.Release(); // Return to pool
CoreData = null!;
} Usage ExamplesExample 1: Creating and Managing Coroutines// Create a coroutine with pooling
var parentThread = state.MainThread;
var luaFunction = new LuaFunction("my_coroutine", async (context, ct) =>
{
// Coroutine logic here
await context.Access.YieldAsync(context.Thread.Stack, ct);
return context.Return("resumed!");
});
var coroutine = LuaCoroutine.Create(parentThread, luaFunction, isProtectedMode: true);
try
{
// Resume the coroutine
var stack = parentThread.Stack;
stack.Push("argument1");
stack.Push("argument2");
var resultCount = await coroutine.ResumeAsync(stack);
// Check results
if (stack.Get(0).TryRead<bool>(out var success) && success)
{
Console.WriteLine("Coroutine succeeded");
// Process results from stack
}
}
finally
{
// Always release coroutines back to pool
if (coroutine.GetStatus() == LuaThreadStatus.Dead)
{
coroutine.Release();
}
} Example 2: Advanced Coroutine with Error Handlingpublic async Task<(bool success, LuaValue[] results)> SafeResumeAsync(LuaCoroutine coroutine, params LuaValue[] args)
{
if (!coroutine.CanResume)
{
return (false, new[] { LuaValue.String("Cannot resume coroutine") });
}
var stack = coroutine.State.MainThread.Stack;
var initialTop = stack.Count;
try
{
// Push arguments
foreach (var arg in args)
{
stack.Push(arg);
}
// Resume coroutine
var resultCount = await coroutine.ResumeAsync(stack);
// Extract results
var results = new LuaValue[resultCount];
var resultSpan = stack.AsSpan()[^resultCount..];
resultSpan.CopyTo(results);
// Check if first result indicates success (for protected mode)
var success = results.Length > 0 && results[0].TryRead<bool>(out var s) ? s : true;
return (success, results);
}
finally
{
// Clean up stack
stack.SetTop(initialTop);
// Release if dead
if (coroutine.GetStatus() == LuaThreadStatus.Dead)
{
coroutine.Release();
}
}
} Example 3: Coroutine Factory Patternpublic class CoroutineManager
{
private readonly LuaState state;
private readonly List<LuaCoroutine> activeCoroutines = new();
public CoroutineManager(LuaState state)
{
this.state = state;
}
public LuaCoroutine CreateCoroutine(LuaFunction function, bool isProtected = true)
{
var coroutine = LuaCoroutine.Create(state.MainThread, function, isProtected);
activeCoroutines.Add(coroutine);
return coroutine;
}
public async Task ResumeAllAsync()
{
for (int i = activeCoroutines.Count - 1; i >= 0; i--)
{
var coroutine = activeCoroutines[i];
if (coroutine.CanResume)
{
await coroutine.ResumeAsync(state.MainThread.Stack);
}
if (coroutine.GetStatus() == LuaThreadStatus.Dead)
{
coroutine.Release();
activeCoroutines.RemoveAt(i);
}
}
}
public void Dispose()
{
foreach (var coroutine in activeCoroutines)
{
if (coroutine.GetStatus() == LuaThreadStatus.Dead)
{
coroutine.Release();
}
}
activeCoroutines.Clear();
}
} Migration GuideFrom v0.4.2 to v0.5// Old (v0.4.2)
var coroutine = new LuaCoroutine(function, isProtectedMode: true);
var context = new LuaFunctionExecutionContext { /* ... */ };
await coroutine.ResumeAsync(context, buffer, cancellationToken);
// New (v0.5)
var coroutine = LuaCoroutine.Create(parentThread, function, isProtectedMode: true);
try
{
var resultCount = await coroutine.ResumeAsync(stack, cancellationToken);
// Process results from stack
}
finally
{
if (coroutine.GetStatus() == LuaThreadStatus.Dead)
{
coroutine.Release(); // Important: return to pool
}
} Performance Benefits
|
This is lovely! And a lot too!! :) A few questions/thoughts:
I will definitely check out the coroutine improvements! |
Thanks, great work! Some things I noticed after a quick look at the code:
|
Thank you for your feedback! Since LuaCompiler.Dump/UnDump is for compiled byte code, I am afraid that Compile/Decompile may not be intuitive. If you still think Compile/Decompile looks good, I will respect that decision. |
For me the "dump" thing is just very odd, but it's not high priority because it's so internal. The main confusion is "undump" because it suggest some sort of "undo" operation, and its meaning is unclear. I suppose C# naming conventions would suggest to use something like I'm okay with |
I remember why I didn't name it ILuaEnvironment. |
Isn't there a problem with the naming of Execute because there is a DoString and a DoFile, and the name Execute is not consistent? |
Sorry!!! I mistake! I'll revert this. |
I'd also vote for consistency: |
How about SerializeToByteCode/DeserializeToPrototype for Dump/UnDump? |
Should this discussion take place in #176? |
I think so. |
Many API changes.

Pass more tests!