Skip to content

Value Marshaling

Quahu edited this page May 24, 2023 · 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).

LuaMarshaler

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 very well through the LuaMarshaler type. Although the marshaler instance is exposed via Lua.Marshaler, accessing it will most likely not have a use case in your code as its functionality is already exposed through other means, such as Lua.Evaluate<T>() or Lua.Stack.PushValue<T>().

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.Delegate1 function
System.Collections.IEnumerable2 table
System.IConvertible matching type from the primitives table

1 All delegates are marshaled using the delegate userdata descriptor. It creates high-performance invokers that support marshaling of both the arguments for the delegate parameters and the return value.

Example:

lua.SetGlobal("int", (int value) => value);

var result = lua.Evaluate<int>("return int(42)");
Console.WriteLine(result); // 42

2 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("ints", new int[] { 1, 2, 3 });

using (var table = lua.Evaluate<LuaTable>("return ints")!)
{
    Console.WriteLine(string.Join(", ", table.ToArray())); // 1, 2, 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 userdata descriptor. If one exists, then the object is marshaled as full userdata using that descriptor.

Example:

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

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 various types of values.

public class MyClass
{
    public string? Text { get; set; }
}
public class MyClassUserDataDecriptor : CallbackBasedUserDataDescriptor
{
    public override string MetatableName => nameof(MyClass);

    // Specifies which callbacks the descriptor supports
    public override CallbackUserDataDescriptorFlags Flags
    {
        get
        {
            // Supports both getting and setting properties;
            return CallbackUserDataDescriptorFlags.Index | CallbackUserDataDescriptorFlags.NewIndex;
        }
    }

    public override int Index(Lua lua, LuaStackValue userData, LuaStackValue key)
    {
        var instance = userData.GetValue<MyClass>()!;

        // Check for `myclass.key`:
        if (key.Type == LuaType.String)
        {
            // This could be done with Reflection, for example:
            switch (key.GetValue<string>())
            {
                case "text":
                {
                    lua.Stack.Push(instance.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 instance = userData.GetValue<MyClass>()!;

        // Check for `myclass.key = value`:
        if (key.Type == LuaType.String)
        {
            // This could be done with Reflection, for example:
            switch (key.GetValue<string>())
            {
                case "text":
                {
                    if (!value.TryGetValue<string>(out var stringValue))
                        lua.RaiseArgumentTypeError(value.Index, "string");

                    instance.Text = stringValue;
                    return 1; // The amount of values returned, i.e. the amount of values we pushed onto the stack.
                }
            }
        }

        return 0;
    }
}
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 allocatating 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. The LuaReference implements the IDisposable interface 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 issue of possibly leaking Lua references, downgrading the performance of the application. 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.

Clone this wiki locally