-
-
Notifications
You must be signed in to change notification settings - Fork 3
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 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
| 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.
| .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
}| .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
IndexandNewIndexin the descriptor are checking whether the key is a string. This is necessary because in Lua usingmyclass.keyis just a shorthand notation formyclass['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
| 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
|
| 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.