Skip to content

Value Marshaling

Quahu edited this page Jun 4, 2024 · 4 revisions

Value Marshaling

Value marshaling is the process of transforming values when they need to be exchanged between managed code (.NET) and native code (Lua).

Quoting Lua manual:

Lua is a dynamically typed language. This means that variables do not have types; only values do. There are no type definitions in the language. All values carry their own type.

This means that when, for example, we want to set a global variable to a specific value from .NET, our only concern is what Lua type our value should use. In Laylua value marshaling is supported through the LuaMarshaler type.

LuaMarshaler is an abstract type which you can implement yourself. By default, a Lua instance will use DefaultLuaMarshaler, accessible via the LuaMarshaler.Default property, which has excellent out-of-the-box marshaling support and will not have to be replaced for most use cases.

Default .NET → Lua marshaling rules
Primitive Types
C# Keyword .NET Type Lua Type
bool System.Boolean boolean
nint/nuint System.IntPtr/System.UIntPtr light userdata
sbyte/byte System.SByte/System.Byte integer1
short/ushort System.Int16/System.UInt16
int/uint System.Int32/System.UInt32
long/ulong2 System.Int64/System.UInt64
float System.Single number
double System.Double
decimal System.Decimal
string System.String string
char System.Char

1 integer is a subtype of the number type in Lua.

2 ulong (System.UInt64) is marshaled as a 64-bit signed integer in this scenario. As a result, the value in Lua may differ from the value in .NET. However, when marshaled back to .NET, it will yield the correct value.


Other Types
.NET Type Lua Type
System.ReadOnlyMemory<char> string
System.Delegate function
System.Collections.IEnumerable1 table
System.IConvertible matching type from the primitives table

1 All enumerables are marshaled as tables. Each item within the enumerable is marshaled individually as if it were a single value. This recursive marshaling process supports enumerables of enumerables. For enumerables containing key-value pairs, such as IEnumerable<KeyValuePair<T1, T2>> or IDictionary, the marshaler sets the table key to the key from the pair and the table value to the value from the pair.

Example:

lua.SetGlobal("array", new[] { 1, 2, 3 });
lua.SetGlobal("dictionary", new Dictionary<string, int>
{
    ["a"] = 1,
    ["b"] = 2,
    ["c"] = 3
});

using (var arrayTable = lua.GetGlobal<LuaTable>("array")!)
using (var dictionaryTable = lua.GetGlobal<LuaTable>("dictionary")!)
{
    var array = table.Values.ToArray();
    var dictionary = table.ToDictionary<string, int>();
    Console.WriteLine(string.Join(", ", array)); // 1, 2, 3
    Console.WriteLine(string.Join(", ", dictionary)); // [a, 1], [b, 2], [c, 3]
}

Special Types
.NET Type Behavior
Laylua.LuaStackValue the value of the stack value is used
Laylua.LuaReference the referenced object is used

When the .NET object is none of these types the marshaler checks for a user data descriptor. If one exists, then the object is marshaled as full userdata using that descriptor. See UserDataDescriptor for details.

Default Lua → .NET marshaling rules
Primitive Types
Lua Type C# Keyword .NET Type
boolean bool System.Boolean
string System.String
light userdata nint/nuint System.IntPtr/System.UIntPtr
number sbyte/byte System.SByte/System.Byte
short/ushort System.Int16/System.UInt16
int/uint System.Int32/System.UInt32
long/ulong System.Int64/System.UInt64
float System.Single
double System.Double
decimal System.Decimal
string System.String
string string System.String
sbyte/byte System.SByte/System.Byte
short/ushort System.Int16/System.UInt16
int/uint System.Int32/System.UInt32
long/ulong System.Int64/System.UInt64
float System.Single
double System.Double
decimal System.Decimal

Other Types
Lua Type .NET Type
string Laylua.Moon.LuaString1
table Laylua.LuaTable2
function Laylua.LuaFunction2
full userdata Laylua.LuaUserData2
thread Laylua.LuaThread2

1 Laylua.Moon.LuaString is an unsafe structure representing a pointer to a Lua C string and its length. It can be used to avoid allocating a new string instance. However, it requires careful handling, as explained in Lua manual.

2 Quoting Lua manual:

Tables, functions, threads, and (full) userdata values are objects: variables do not actually contain these values, only references to them. Assignment, parameter passing, and function returns always manipulate references to such values; these operations do not imply any kind of copy.
Laylua follows the same principal and marshals tables, functions, full userdata, and threads as the types specified in the table. All these types share the base type Laylua.LuaReference which represents the reference to the object within the Lua registry. LuaReference implements IDisposable and when disposed releases the reference to the object within Lua. The object itself becomes eligible for garbage collection when neither you nor Lua have any remaining references to it.

No matter the Lua type, you can get the value as object (System.Object). This is useful for debugging purposes or for writing simple but flexible code. This does have the risk of possibly leaking Lua references, downgrading the performance of the application and causing a memory leak. You can use the various LuaReference.Dispose() overloads to dispose of any LuaReference instances masked as objects.

With this in mind, it is recommended that you use generic overloads when possible as that prevents the boxing of value types and reduces the risk of not disposing LuaReference instances.

Userdata

Quoting Lua manual:

The type userdata is provided to allow arbitrary C data to be stored in Lua variables. (...) Userdata has no predefined operations in Lua, except assignment and identity test. By using metatables, the programmer can define operations for full userdata values (see §2.4). Userdata values cannot be created or modified in Lua, only through the C API. This guarantees the integrity of data owned by the host program and C libraries.

The default marshaler uses a UserDataDescriptorProvider instance to determine how to convert .NET objects to Lua userdata instances. The UserDataDescriptor returned from it determines what the given userdata supports.

No type descriptors are set by default. This aligns with the idea that Laylua prioritizes sandboxing, allowing only the exposure of specific functionality to Lua code.

UserDataDescriptor is an abstract type which you can implement yourself. However, because creating metatables is a repetitive and delicate process, some out-of-the-box implementations are provided to simplify this process:

  • CallbackUserDataDescriptor - abstracts away the process of creating the userdata's metatable. You can implement this type and simply define the callbacks you want the userdata instance to support.
  • TypeUserDataDescriptor - an implementation of CallbackUserDataDescriptor that is bound to a System.Type. It has the following default implementations:
    • DefinitionTypeUserDataDescriptor - represents a type descriptor for a type
    • InstanceTypeUserDataDescriptor - represents a type descriptor for an instance of a type

In most cases, you will be interacting only with DefinitionTypeUserDataDescriptor and InstanceTypeUserDataDescriptor and methods related to them.

Practical Examples

Example of mapping user-defined .NET types to Lua.
public class MyClass
{
    public string? Text { get; set; }

    public void Print()
    {
        Console.WriteLine(Text);
    }
}

// One-time setup.
UserDataDescriptorProvider.Default.SetInstanceDescriptor<MyClass>();

using (var lua = new Lua())
{
    lua.SetGlobal("value", new MyClass());
    lua.Execute("""
                value.Text = 'Hello, World!'
                value:Print()
                """);
}

// This example prints the following:
//      Text: Hello, World!

For static types and for static members of types and generally to represent a System.Type, you would set definition descriptors instead. The example from above modified to include a type definition descriptor:

public class MyClass
{
    public string? Text { get; set; }

    public void Print()
    {
        Console.WriteLine(Text);
    }

    public static MyClass Create()
    {
        return new MyClass();
    }
}

// One-time setup.
UserDataDescriptorProvider.Default.SetDefinitionDescriptor<MyClass>();
UserDataDescriptorProvider.Default.SetInstanceDescriptor<MyClass>();

using (var lua = new Lua())
{
    lua.SetGlobal("MyClass", typeof(MyClass));
    lua.Execute("""
                value = MyClass.Create()
                value.Text = 'Hello, World!'
                value:Print()
                """);
}

// This example prints the following:
//      Text: Hello, World!
Example of creating and using a CallbackUserDataDescriptor.
// One-time setup.
UserDataDescriptorProvider.Default.SetDescriptorForType<MyClass>(new MyClassUserDataDecriptor());

using (var lua = new Lua())
{
    lua.SetGlobal("myclass", new MyClass());
    
    lua.Execute("myclass.text = 'Hello, World!'");
    
    var result = lua.Evaluate<string>("return myclass.text");
    Console.WriteLine(result); // Hello, World!
}

Note how both Index and NewIndex in the descriptor are checking whether the key is a string. This is necessary because in Lua using myclass.key is just a shorthand notation for myclass['key']. In other words, Lua allows indexing with values of various types.

public class MyClass
{
    public string? Text { get; set; }
}

public class MyClassUserDataDecriptor : CallbackBasedUserDataDescriptor
{
    public override string MetatableName => nameof(MyClass); // Must be unique!

    // Specifies which callbacks the descriptor supports
    public override CallbackUserDataDescriptorFlags Flags
    {
        get => CallbackUserDataDescriptorFlags.Index | CallbackUserDataDescriptorFlags.NewIndex;
    }

    public override int Index(Lua lua, LuaStackValue userData, LuaStackValue key)
    {
        var myClass = userData.GetValue<MyClass>()!;
        if (key.Type == LuaType.String)
        {
            var stringKey = key.GetValue<string>();
            if (stringKey == "text")
            {
                lua.Stack.Push(myClass.Text);
                return 1; // The amount of values returned, i.e. the amount of values we pushed onto the stack.
            }
        }

        return 0;
    }

    public override int NewIndex(Lua lua, LuaStackValue userData, LuaStackValue key, LuaStackValue value)
    {
        var myClass = userData.GetValue<MyClass>()!;
        if (key.Type == LuaType.String)
        {
            var stringKey = key.GetValue<string>();
            if (stringKey == "text")
            {
                if (!value.TryGetValue<string>(out var stringValue))
                {
                    lua.RaiseArgumentTypeError(value.Index, "string");
                }

                myClass.Text = stringValue;
            }
        }

        return 0;
    }
}
Clone this wiki locally