Skip to content

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

Merged
merged 207 commits into from
Jun 25, 2025
Merged

version 0.5 #135

merged 207 commits into from
Jun 25, 2025

Conversation

Akeit0
Copy link
Collaborator

@Akeit0 Akeit0 commented Apr 27, 2025

Many API changes.
Pass more tests!
image

@Akeit0 Akeit0 requested a review from nuskey8 April 29, 2025 13:26
@Akeit0
Copy link
Collaborator Author

Akeit0 commented Apr 29, 2025

@nuskey8
The basic structure is done, but I'd like to see API adjustments, etc.
If you follow the steps from ConsoleApp2, it should be easy to understand for now.

@nuskey8
Copy link
Owner

nuskey8 commented Apr 30, 2025

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.

@CodeSmile-0000011110110111
Copy link

CodeSmile-0000011110110111 commented Jun 14, 2025

Here's one for Unity support. I noticed Jetbrains Rider wouldn't open a .lua file at the expected line/column number when I was trying to hyperlink Lua logs. After some research I found that it started to work after I added "lua" as file extension under ProjectSettings/Editor.

I don't know if this has other benefits for interoperation with external tools, perhaps showing .lua files in the IDE "explorer" view alongside .cs files.

So I wrote a class that automates adding "lua". It makes that check when the class is first instantiated, ie either when it was added to the project and once whenever the project opens (by using Awake).

using System;
using System.Linq;
using UnityEditor;
using UnityEngine;

namespace Editor.Lua.Unity
{
	internal sealed class AddLuaToUserFileExtensions : ScriptableSingleton<AddLuaToUserFileExtensions>
	{
		private const String LuaExtension = "lua";

		[InitializeOnLoadMethod] private static AddLuaToUserFileExtensions OnLoad() => instance; // auto-create the singleton

		private static void CheckAndAddLuaExtension()
		{
			if (EditorSettings.projectGenerationUserExtensions.Contains(LuaExtension) == false)
			{
				var extensions = EditorSettings.projectGenerationUserExtensions.ToList();
				// prevent possible duplicates, no harm just to keep it clean
				extensions.Remove("LUA");
				extensions.Remove("Lua");
				extensions.Add(LuaExtension);
				EditorSettings.projectGenerationUserExtensions = extensions.ToArray();
			}
		}

		private void Awake() => CheckAndAddLuaExtension();
	}
}

Feel free to use and modify it for Lua.Unity.

This is where it'll be added to:
image

You can test opening external files with:
UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(path, line, column);

@CodeSmile-0000011110110111

FYI current 0.5 dev branch doesn't build due to missing TimeProvider type.

@Akeit0
Copy link
Collaborator Author

Akeit0 commented Jun 15, 2025

@CodeSmile-0000011110110111

FYI current 0.5 dev branch doesn't build due to missing TimeProvider type.

Thank you!! Unfortunately, I was able to build it in my environment....

@CodeSmile-0000011110110111
Copy link

CodeSmile-0000011110110111 commented Jun 15, 2025

Thanks, got the update, I can build, but if I open my project in Unity 6 with the new DLL I get this error:

P:\de.codesmile.luny\Runtime\Lua\LunyLua.cs(90,37): error CS0012: The type 'TimeProvider' is defined in an assembly that is not referenced. You must add a reference to assembly 'Microsoft.Bcl.TimeProvider, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'.

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 precompiledReferences according to this:
https://github.com/Cysharp/R3/blob/main/src/R3.Unity/Assets/R3.Unity/Runtime/R3.Unity.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:
image
... and the Bcl.AsyncInterfaces.dll as well.

@CodeSmile-0000011110110111

I'm a little confused with LuaThreadAccessAccessExtensions. Both DoString and DoBytes call Load and then DoClosure(access, closure). But DoFile deviates from this pattern by calling access.RunAsync(closure).

Is there a reason for this or does this simply need some more work to achieve consistency?

@Akeit0
Copy link
Collaborator Author

Akeit0 commented Jun 20, 2025

I'm a little confused with LuaThreadAccessAccessExtensions. Both DoString and DoBytes call Load and then DoClosure(access, closure). But DoFile deviates from this pattern by calling access.RunAsync(closure).

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.

@Akeit0
Copy link
Collaborator Author

Akeit0 commented Jun 23, 2025

@nuskey8 This is not perfect, but it helps to know the changes.
Be aware that there are hallucinations.

Lua-CSharp v0.5 API Change generated by Claude Code

Lua-CSharp v0.5 API Changes

This 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 Changes

1. LuaState Constructor and Platform System

The 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 Changes

All 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 System

Direct thread access replaced with versioned LuaThreadAccess:

// 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 Changes

Module 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 Features

Platform Abstraction Layer

New LuaPlatform class centralizes all platform-specific operations:

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 Interface

New 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 Abstraction

Complete 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 Interface

Operating system operations are now abstracted:

public interface ILuaOsEnvironment
{
    string? GetEnvironmentVariable(string name);
    ValueTask Exit(int exitCode, CancellationToken cancellationToken);
    double GetTotalProcessorTime();
}

LuaThreadAccess API

Core Concept

LuaThreadAccess is a versioned struct that provides safe access to thread operations. The version system prevents use-after-invalidation bugs by tracking when thread state changes:

Version Tracking Purpose:

  • Thread State Changes: Version increments when thread transitions between states (Running, Suspended, Dead)
  • Call Stack Modifications: Version changes when functions are called or return, affecting the call stack
  • Access Invalidation: When a thread's version changes, all existing LuaThreadAccess instances with older versions become invalid
  • Memory Safety: Prevents accessing thread state after the thread has been modified or recycled

Call Stack Pop Version Changes:
The version is managed carefully during call stack operations:

  • Function Calls: Version increments when entering a function (push frame)
  • Function Returns: Version reverts to previous value when returning (pop frame)
  • Error Unwinding: Version changes during error handling when stack frames are unwound
  • Coroutine Operations: Version changes persist when coroutines yield or are resumed from different contexts

The version is restored after normal function returns, so LuaThreadAccess obtained before a call remains valid after the call completes. However, access becomes invalid in cases of abnormal control flow (errors, yields) or when accessed from different execution contexts.

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 Methods

All implemented in LuaThreadAccessExtensions:

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 Operations

void Push(LuaValue value);
void Push(params ReadOnlySpan<LuaValue> span);
LuaValue Pop();
void Pop(int count);
LuaReturnValuesReader ReadReturnValues(int argumentCount);

Arithmetic Operations

ValueTask<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 Operations

ValueTask<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 Operations

ValueTask<LuaValue> GetTable(LuaValue table, LuaValue key, CancellationToken ct = default);
ValueTask SetTable(LuaValue table, LuaValue key, LuaValue value, CancellationToken ct = default);

Function Calls

ValueTask<LuaValue[]> Call(LuaValue function, ReadOnlySpan<LuaValue> arguments, CancellationToken ct = default);
ValueTask<int> Call(int funcIndex, int returnBase, CancellationToken ct = default);

Compilation System Changes

Chunk to Prototype Rename

The Chunk class has been renamed and restructured:

// 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

  • Source → ChunkName
  • Instructions → Code

LuaCompiler API

New 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 Methods

Load 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);

LuaStateExtensions

Convenience 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 Changes

ILuaModuleLoader Interface

Enhanced with existence checking:

public interface ILuaModuleLoader
{
    bool Exists(string moduleName);
    ValueTask<LuaModule> LoadAsync(string moduleName, CancellationToken cancellationToken);
}

LuaModule Types

Support 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 Changes

Enhanced 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 Changes

Bytecode Asset Support

New 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 Integration

Unity-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 Loaders

Updated 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 Guide

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

  1. Security: Platform abstraction enables sandboxing for untrusted scripts
  2. Performance: Bytecode support and versioned thread access reduce overhead
  3. Reliability: Versioned access prevents use-after-free bugs
  4. Flexibility: Multiple platform implementations for different environments
  5. Compatibility: Maintains most existing API surface while adding new capabilities
  6. Unity Integration: Enhanced Unity support with bytecode assets and platform-specific implementations

Summary

Version 0.5 represents a major architectural evolution focusing on:

  • Platform Independence: Abstract away platform-specific operations
  • Security: Better sandboxing capabilities for embedded scenarios
  • Performance: Optimized memory usage, bytecode support, and versioned access
  • Reliability: Thread-safe access patterns and better error handling
  • Unity Integration: Enhanced Unity package with bytecode support and debugging tools

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)

Overview

Version 0.5 introduces a comprehensive debug library (DebugLibrary) that was completely missing in v0.4.2. This provides standard Lua debugging capabilities for introspection and runtime manipulation.

Debug Library Functions

The 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 Functions

Local 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 Examples

Example 1: Stack Inspection

function 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 Introspection

function 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)

Overview

Version 0.5 introduces LightUserData support, which was missing in v0.4.2. LightUserData represents C# objects as lightweight Lua values without full userdata overhead.

LuaValueType Enum Addition

public enum LuaValueType : byte
{
    Nil,
    Boolean, 
    String,
    Number,
    Function,
    Thread,
    LightUserData, // New in v0.5
    UserData,
    Table,
}

LightUserData vs UserData

Feature LightUserData UserData
Metatable No Yes (via ILuaUserData)
Use Case Simple C# object references Objects with metamethods

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 Examples

Example 1: Passing C# Objects to Lua

public 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)

Overview

Version 0.5 introduces a clear distinction between C#-implemented functions (CSharpClosure) and Lua-compiled functions (LuaClosure), replacing the generic Closure class from v0.4.2.

Changes from v0.4.2

Before (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 Features

Upvalue 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 Examples

Example 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 Types

var 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 Enhancements

Overview

Version 0.5 significantly enhances the LuaCoroutine API with better performance, memory management, and more intuitive interfaces while maintaining compatibility with the coroutine library.

Key Changes from v0.4.2

Constructor 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 Features

1. 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 Examples

Example 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 Handling

public 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 Pattern

public 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 Guide

From 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

  1. Object Pooling: Eliminates GC pressure from frequent coroutine creation
  2. Zero-Copy Results: Span-based result handling avoids array allocations
  3. Stack-Based Operations: Direct stack manipulation instead of buffer copying
  4. Reduced Memory Footprint: No fixed buffer allocation per coroutine
  5. Better Resource Management: Explicit lifecycle with pooling

@CodeSmile-0000011110110111

This is lovely! And a lot too!! :)

A few questions/thoughts:

  1. What bumps the ThreadAccess version? Does the version also decrement on some operation?

  2. LuaCompiler: are Dump/UnDump based on Lua notation? Because "dump" to me means "carelessly put something somewhere". One can also "take a dump". Not sure how one would "undump" that. :)
    "Dump" feels wrong to me in this context. I only use "dump" when I am writing debug info with no intention of reading it back in.
    The Compiler compiles or converts to bytecode, and creates a prototype from compiled code or bytecode.

  3. LuaAsset/LuacAsset => Is there a "DoAsset(LuaAssetBase luaAsset)" extension? That should handle switching between DoString or DoBytes based on the asset type.
    I know "luac" is the default extension but it kinda feels clunky and not C#-style to have a "LuacAsset". LuaTextAsset and LuaBinaryAsset (or similar) would be more relatable.

  4. Key Benefits and Summary are quite repetitive. Could be rolled into a single heading.

  5. LightUserData is very welcome! GC Tracking "Minimal" means "no Lua GC"? Because in PIL 28.5 they mention lightuserdata is unmanaged:

Like numbers, light userdata do not need to be managed by the garbage collector (and are not).

I will definitely check out the coroutine improvements!

@nuskey8
Copy link
Owner

nuskey8 commented Jun 25, 2025

Thanks, great work!

Some things I noticed after a quick look at the code:

  • LuaCompiler.Dump/UnDump seem like a non-intuitive API. Compile/Decompile etc would be better.

  • DoBytes doesn't seem like a good name. If you're just running bytecode, Execute etc would be better.

  • BomUtility and LuaPlatformUtility is an internal utility so doesn't need to be public.

  • ILuaOsEnvironment is better than ILuaOSEnvironment. ILuaEnvironment may be better because it's not OS-related functionality.

  1. DummyInputStream should be internal. Users probably won't use it directly.

@Akeit0
Copy link
Collaborator Author

Akeit0 commented Jun 25, 2025

@nuskey8

Thank you for your feedback!
I will correct them.
However, there is one thing I'm worried about.

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.

@CodeSmile-0000011110110111
Copy link

CodeSmile-0000011110110111 commented Jun 25, 2025

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 ToByteCode, ToOpCodes, ToClosure when you convert one type to another, like in ToString, ToArray.

I'm okay with DoBytes. Alternatives might be DoChunk or DoByteCode.

@Akeit0
Copy link
Collaborator Author

Akeit0 commented Jun 25, 2025

@nuskey8

ILuaOsEnvironment is better than ILuaOSEnvironment. ILuaEnvironment may be better because it's not OS-related functionality.

I remember why I didn't name it ILuaEnvironment.
I thought it might be confusing with LuaState.Environment /_ENV, so I added OS.
If there is a good naming, I would like to change it.

@Akeit0
Copy link
Collaborator Author

Akeit0 commented Jun 25, 2025

DoBytes doesn't seem like a good name. If you're just running bytecode, Execute etc would be better.

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?

@Akeit0 Akeit0 merged commit 6ea0a15 into main Jun 25, 2025
@Akeit0
Copy link
Collaborator Author

Akeit0 commented Jun 25, 2025

Sorry!!! I mistake! I'll revert this.

@CodeSmile-0000011110110111

I'd also vote for consistency: DoWhatever but with "Do" it shall begin. ;)

@Akeit0 Akeit0 mentioned this pull request Jun 25, 2025
@Akeit0
Copy link
Collaborator Author

Akeit0 commented Jun 25, 2025

How about SerializeToByteCode/DeserializeToPrototype for Dump/UnDump?
Very direct, but easy to understand.

@nuskey8
Copy link
Owner

nuskey8 commented Jun 26, 2025

Should this discussion take place in #176?

@Akeit0
Copy link
Collaborator Author

Akeit0 commented Jun 26, 2025

Should this discussion take place in #176?

I think so.

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.

4 participants