-
-
Notifications
You must be signed in to change notification settings - Fork 2
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
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 /ulong 2 |
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.Delegate |
function |
System.Collections.IEnumerable 1 |
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]
}
.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
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.LuaString 1 |
table | Laylua.LuaTable 2 |
function | Laylua.LuaFunction 2 |
full userdata | Laylua.LuaUserData 2 |
thread | Laylua.LuaThread 2 |
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 object
s.
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.
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 ofCallbackUserDataDescriptor
that is bound to aSystem.Type
. It has the following default implementations:
In most cases, you will be interacting only with DefinitionTypeUserDataDescriptor
and InstanceTypeUserDataDescriptor
and methods related to them.
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
andNewIndex
in the descriptor are checking whether the key is a string. This is necessary because in Lua usingmyclass.key
is just a shorthand notation formyclass['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; } }