layout | title | subtitle |
---|---|---|
tutorial |
Sharing objects |
Let Lua and C# speak each other. |
Note : Some features listed in this page reflect the current status of the master branch (so, they might be features missing from the latest release).
A handy feature of MoonSharp is the ability to share a .NET object with a script.
A type by default will share all its public methods, public properties, public events and public fields with Lua scripts. The MoonSharpVisible attribute can be used to override this default visibility.
It is recommended to use dedicated objects as an interface between CLR code and script code (as opposed to exposing the application internal model to the script). A number of design patterns (Adapter, Facade, Proxy, etc.) may help in designing such an interface layer. This is particularly important to:
- Limit what scripts can and cannot do (security! do you want your mod authors to find a way to delete end user personal files ?)
- Offer an interface which is meaningful for the script authors
- Document the interface separately
- Allow the internal logic and model to change without breaking scripts
For these reasons MoonSharp by default requires an explicit registration of the types which will be available to scripts.
If you are in a scenario where scripts can be trusted, you can globally enable autoregistration with UserData.RegistrationPolicy = InteropRegistrationPolicy.Automatic;
. It's dangerous, you have been warned.
So, let's see what we have on the menu:
- Let's have a word about Type descriptors first] - a little theory explaining what happens behind the scenes and how you can override the whole interop system
- Keep it simple - the simplest way to get started
- A little more complex - we dig in, adding a little more complexity and details
- Calling static members - how to call statics
- Should ':' or '.' be used ? - simple question about how to call methods
- Overloads - how overloads are handled
- ByRef parameters (ref/out in C#) - how ref/out params are handled
- Indexers - how to handle indexers
- Operators and metamethods on userdata - how to overload operators, etc.
- Extension Methods - how to use extension methods
- Events - how to use events
- Interop Access Modes - what the interop access mode is and how it works
- Changing visibility with MoonSharpHidden and MoonSharpVisible - how to override the visibility of members
- Removing members - how to remove the visibility of members
A lot, so let's start.
First some little theory of how interop is implemented. Every CLR type is wrapped into a "type descriptor" which has the role of describing the CLR type to scripts. Registering a Type
for
interop means associating the Type
with a descriptor (which MoonSharp can create itself) which will be used to dispatch methods, properties, etc.
From the next section on, we will refer to the "automatic" descriptors MoonSharp offers, but you can implement your own descriptors for speed, added funcionality, security, etc.
If you want to implement your own descriptors (which is not easy and should not be done unless you need to) you can follow these paths:
- Create an ad-hoc
IUserDataDescriptor
to describe your own type(s) - this is the hardest way - Have your type implement the
IUserDataType
interface. This is easier, but means you cannot handle static members without an object instance. - Extend or embed
StandardUserDataDescriptor
and change the aspects you need while keeping the rest of the behaviour.
In order to help the creation of descriptors, the following classes are available:
StandardUserDataDescriptor
- this is the type descriptor MoonSharp implementsStandardUserDataMethodDescriptor
- this is a descriptor for a single method/functionStandardUserDataOverloadedMethodDescriptor
- this is a descriptor for overloaded and/or extended methodsStandardUserDataPropertyDescriptor
- this is a descriptor for a single propertyStandardUserDataFieldDescriptor
- this is a descriptor for a single field
A little note about interop with value types as userdata.
Just as if calling a function passing a value type as a parameter, the script would operate on a copy of the userdata, so, for example, changing a field in the userdata would not reflect on the original value. Again, this is not any different from standard behavior of value types, but it's enough to catch people by surprise.
Additionally, value types do not support the whole spectrum of optimizations as reference types do, so some operations might be slower on value types than reference types.
Ok, let's go with the first example.
{% highlight csharp %}
[MoonSharpUserData] class MyClass { public double calcHypotenuse(double a, double b) { return Math.Sqrt(a * a + b * b); } }
double CallMyClass1()
{
string scriptCode = @"
return obj.calcHypotenuse(3, 4);
";
// Automatically register all MoonSharpUserData types
UserData.RegisterAssembly();
Script script = new Script();
// Pass an instance of MyClass to the script in a global
script.Globals["obj"] = new MyClass();
DynValue res = script.DoString(scriptCode);
return res.Number;
}
{% endhighlight %}
Here we:
- Defined a class with
[MoonSharpUserData]
attribute - Passed an instance of a
MyClass
object as a global in the script - Invoked a
MyClass
method from a script. All the mapping rules for callbacks apply
Let's try a little more complex example.
{% highlight csharp %}
class MyClass { public double CalcHypotenuse(double a, double b) { return Math.Sqrt(a * a + b * b); } }
static double CallMyClass2()
{
string scriptCode = @"
return obj.calcHypotenuse(3, 4);
";
// Register just MyClass, explicitely.
UserData.RegisterType<MyClass>();
Script script = new Script();
// create a userdata, again, explicitely.
DynValue obj = UserData.Create(new MyClass());
script.Globals.Set("obj", obj);
DynValue res = script.DoString(scriptCode);
return res.Number;
}
{% endhighlight %}
The big differences here are:
- No
[MoonSharpUserData]
attribute. We don't need it anymore. - Instead of
RegisterAssembly
, we callRegisterType
to register a specific type. - We create the userdata
DynValue
explicitely.
Also, note how the method was called CalcHypotenuse
in C# code, but is called as calcHypotenuse
by Lua scripts.
As long as the other versions do not exist, MoonSharp automatically adjusts the case in some limited ways to match members, for better consistency between different languages syntax conventions.
For example, a member called SomeMethodWithLongName
can be accessed from a lua script also as someMethodWithLongName
or some_method_with_long_name
.
Let's say our class has the calcHypotenuse
method static.
{% highlight csharp %}
[MoonSharpUserData] class MyClassStatic { public static double calcHypotenuse(double a, double b) { return Math.Sqrt(a * a + b * b); } }
{% endhighlight %}
We can call it in two ways.
First way - Static methods can be called from an instance transparently - no need to do anything, everything is automatic
{% highlight csharp %}
double MyClassStaticThroughInstance()
{
string scriptCode = @"
return obj.calcHypotenuse(3, 4);
";
// Automatically register all MoonSharpUserData types
UserData.RegisterAssembly();
Script script = new Script();
script.Globals["obj"] = new MyClassStatic();
DynValue res = script.DoString(scriptCode);
return res.Number;
}
{% endhighlight %}
Alternative way - A placeholder userdata can be created, by directly passing the type (or using UserData.CreateStatic
method) :
{% highlight csharp %}
double MyClassStaticThroughPlaceholder()
{
string scriptCode = @"
return obj.calcHypotenuse(3, 4);
";
// Automatically register all MoonSharpUserData types
UserData.RegisterAssembly();
Script script = new Script();
script.Globals["obj"] = typeof(MyClassStatic);
DynValue res = script.DoString(scriptCode);
return res.Number;
}
{% endhighlight %}
A good question is, should (given the code in the above examples) this syntax be used
{% highlight lua %}
return obj.calcHypotenuse(3, 4);
{% endhighlight %}
or this ?
{% highlight lua %}
return obj:calcHypotenuse(3, 4);
{% endhighlight %}
99.999% of the time, it makes no difference. MoonSharp knows that a call is being done on a userdata and behaves accordingly.
There are corner cases where it might make a difference - for example if a property returns a delegate and you are going to call that delegate immediately, with the original object as an instance. It's a remote scenario, and you have to handle it manually when that happens.
Overloaded methods are supported. The dispatch of overloaded method is somewhat a dark magic and is not as deterministic as C# overload dispatch is. This is due to the fact that some ambiguities exist. For example, an object can declare these two methods:
{% highlight csharp %}
void DoSomething(int i) { ... } void DoSomething(float f) { ... }
{% endhighlight %}
How can MoonSharp know which method to dispatch to, given that all numbers in Lua are double ?
To solve this issue, MoonSharp calculates an heuristic factor for all overloads given the input types and chooses the best overload. If you think MoonSharp is resolving an overload in a wrong way, please report to the forums or discord, for the heuristic to be calibrated.
MoonSharp tries as much to be stable with the heuristic weights, and in case of a draw of scores between methods, it always deterministically choses the same one (to provide a consistent experience among builds and platforms).
This said, it's entirely possible that MoonSharp picks an overload which is different than the one you think of. It's extremely important then that overloads perform equivalent jobs so that the impact of calling the wrong overload is minimized. This should be a best practice anyway, but it's worth reinforcing the concept here.
ByRef
method parameters are correctly marshalled by MoonSharp, as multiple return values.
This support is not without side effects, as methods with ByRef
parameters cannot be optimized.
Let's say we have this C# method (exposed in a myobj
userdata for the sake of the argument)
{% highlight csharp %}
public string ManipulateString(string input, ref string tobeconcat, out string lowercase) { tobeconcat = input + tobeconcat; lowercase = input.ToLower(); return input.ToUpper(); }
{% endhighlight %}
We can call (and get the results) the method from Lua code in this way:
{% highlight lua %}
x, y, z = myobj:manipulateString('CiAo', 'hello');
-- x will be "CIAO" -- y will be "CiAohello" -- z will be "ciao"
{% endhighlight %}
While supported, ByRef params causes the method to always be invoked using reflection, thus potentially slowing down performance on non-AOT platforms (AOT platforms are already slow.. send your complaints to Apple, not me).
C# allows indexer methods to be created. For example:
{% highlight csharp %}
class IndexerTestClass { Dictionary<int, int> mymap = new Dictionary<int, int>();
public int this[int idx]
{
get { return mymap[idx]; }
set { mymap[idx] = value; }
}
public int this[int idx1, int idx2, int idx3]
{
get { int idx = (idx1 + idx2) * idx3; return mymap[idx]; }
set { int idx = (idx1 + idx2) * idx3; mymap[idx] = value; }
}
}
{% endhighlight %}
As an extension to the Lua language, MoonSharp allows an expression list inside brackets to index userdata.
For example these are valid lines of code, given that o
is an instance of the above class:
{% highlight lua %}
-- sets the value of an indexer o[5] = 19;
-- use the value of an indexer x = 5 + o[5];
-- sets the value of an indexer using multiple indices (not standard Lua!) o[1,2,3] = 19;
-- use the value of an indexer using multiple indices (not standard Lua!) x = 5 + o[1,2,3];
{% endhighlight %}
Note that using multiple indices on anything which is not a userdata will raise an error. This includes
scenarios going through metamethods, but if the __index
field of the metatable is set to a userdata (also, recursively),
multi-indexing is supported.
In short, this works:
{% highlight lua %}
m = { __index = o, __newindex = o }
t = { }
setmetatable(t, m);
t[10,11,12] = 1234; return t[10,11,12];";
{% endhighlight %}
and this does not:
{% highlight lua %}
m = {
-- we can't even write meaningful functions here, but let's pretend...
__index = function(obj, idx) return o[idx] end,
__newindex = function(obj, idx, val) end
}
t = { }
setmetatable(t, m);
t[10,11,12] = 1234; return t[10,11,12];";
{% endhighlight %}
Overloaded operators are supported.
Following is the description on how the standard descriptor dispatches operators, but you can see working examples in this unit test code.
First, if one or more static methods decorated with MoonSharpUserDataMetamethod are implemented, these are used to dispatch the corresponding metamethod. Note that these methods exist, they will take over any other of the following criteria.
__pow
, __concat
, __call
, __pairs
and __ipairs
can only be implemented this way (short of using a custom descriptor).
For example these will implement the concat (..
) operator:
{% highlight csharp %} [MoonSharpUserDataMetamethod("__concat")] public static int Concat(ArithmOperatorsTestClass o, int v) { return o.Value + v; }
[MoonSharpUserDataMetamethod("__concat")] public static int Concat(int v, ArithmOperatorsTestClass o) { return o.Value + v; }
[MoonSharpUserDataMetamethod("__concat")] public static int Concat(ArithmOperatorsTestClass o1, ArithmOperatorsTestClass o2) { return o1.Value + o2.Value; } {% endhighlight %}
Arithmetic operators are automatically handled by operator overloads if found.
{% highlight csharp %} public static int operator +(ArithmOperatorsTestClass o, int v) { return o.Value + v; }
public static int operator +(int v, ArithmOperatorsTestClass o) { return o.Value + v; }
public static int operator +(ArithmOperatorsTestClass o1, ArithmOperatorsTestClass o2) { return o1.Value + o2.Value; } {% endhighlight %}
it will be possible to use the operator +
between numbers and this object in Lua scripts.
The addition, subtraction, multiplication, division, modulus, and unary negation operators are supported this way.
Equality operators (==
and ~=
) are automatically resolved using System.Object.Equals
.
Comparison operators (<
, >=
, etc.) are automatically resolved using IComparable.CompareTo
, if the object implements IComparable
.
The length (#
) operator is dispatched to the Length
or Count
properties if the object implements those properties.
Finally the __iterator
metamethod is automatically dispatched to GetEnumerator
, if the class implements System.Collections.IEnumerable
.
Extension methods are supported.
Extension methods must be registered with UserData.RegisterExtensionType
or through a RegisterAssembly(<assembly>, true)
.
The first will register a single type containing extension methods, the second registers all extension types contained in the specified assembly.
Extension methods are resolved along with other overloads on the methods.
Events are also supported, but in a rather minimalistic way. Only events matching these constraints are supported:
- The event must be declared in a reference type
- The event must implement both add and remove methods
- The event handler must have a return type of System.Void (in VB.NET, must be a Sub)
- The event handler must have 16 parameters or less
- The event handler must not have value type parameters or by-ref parameters
- The event handler signature must not contain pointers or unresolved generics
- All the parameters of the event handler must be convertible to MoonSharp types
These constraints are present to avoid building code at runtime as much as possible.
While they might seem limiting, for the most part they actually reflect some best practices in the design of events; they are more than enough
to support event handlers of EventHandler
and EventHandler<T>
types, which are by far the most common ones (provided at least EventArgs
is registered as a user data).
Here is a simple example using an event:
{% highlight csharp %} class MyClass { public event EventHandler SomethingHappened;
public void RaiseTheEvent()
{
if (SomethingHappened != null)
SomethingHappened(this, EventArgs.Empty);
}
}
static void Events()
{
string scriptCode = @"
function handler(o, a)
print('handled!', o, a);
end
myobj.somethingHappened.add(handler);
myobj.raiseTheEvent();
myobj.somethingHappened.remove(handler);
myobj.raiseTheEvent();
";
UserData.RegisterType<EventArgs>();
UserData.RegisterType<MyClass>();
Script script = new Script();
script.Globals["myobj"] = new MyClass();
script.DoString(scriptCode);
} {% endhighlight %}
Note how the event is raised by Lua code this time, but it might be raised by C# as well, without any issue.
Adding and removing event handlers are slow operations, being performed with reflection under a thread lock. On the other side, there aren't big performance penalties in handling events themselves.
If you typed all the examples so far in an IDE you might have noticed that most methods have an optional parameter of InteropAccessMode
type.
An InteropAccessMode
defines how the standard descriptors will handle callbacks to CLR things. The following values are available:
There is a UserData.DefaultAccessMode
static property to specify which value is to be considered the default (currently, it's LazyOptimized
, unless changed).
Reflection | Optimization is not performed and reflection is used everytime to access members. This is the slowest approach but saves a lot of memory if members are seldomly used. |
---|---|
LazyOptimized | This is a hint, and MoonSharp is free to "downgrade" this to Reflection . Optimization is done on the fly the first time a member is accessed. This saves memory for all members that are never accessed, at the cost of an increased script execution time. |
Preoptimized | This is a hint, and MoonSharp is free to "downgrade" this to Reflection . Optimization is done in a background thread which starts at registration time. If a member is accessed before optimization is completed, reflection is used. |
BackgroundOptimized | This is a hint, and MoonSharp is free to "downgrade" this to Reflection . Optimization is done at registration time. |
HideMembers | Members are simply not accessible at all. Can be useful if you need a userdata type whose members are hidden from scripts but can still be passed around to other functions. See also AnonWrapper and AnonWrapper<T> . |
Default | Use the default access mode |
Note that many modes - specifically
LazyOptimized
,Preoptimized
andBackgroundOptimized
- are just "hints" and MoonSharp is free to downgrade them toReflection
. This happens, for example, in the case of platforms where code is compiled ahead of time, like the iPhone and the iPad.
Changing visibility with MoonSharpHidden and MoonSharpVisible
It's possible to use the MoonSharpHidden
and/or MoonSharpVisible
attribute to override the default visibility of members (MoonSharpHidden
is a shortcut for MoonSharpVisible(false)
). Here are some examples with comments - nothing hard:
{% highlight csharp %} public class SampleClass { // Not visible - it's private private void Method1() { } // Visible - it's public public void Method2() { } // Visible - it's private but forced visible by attribute [MoonSharpVisible(true)] private void Method3() { } // Not visible - it's public but forced hidden by attribute [MoonSharpVisible(false)] public void Method4() { } // Not visible - it's public but forced hidden by attribute [MoonSharpHidden] public void Method4() { }
// Not visible - it's private
private int Field1 = 0;
// Visible - it's public
public int Field2 = 0;
// Visible - it's private but forced visible by attribute
[MoonSharpVisible(true)]
private int Field3 = 0;
// Not visible - it's public but forced hidden by attribute
[MoonSharpVisible(false)]
public int Field4 = 0;
// Not visible at all - it's private
private int Property1 { get; set; }
// Read/write - it's public
public int Property2 { get; set; }
// Readonly - it's public, but the setter is private
public int Property3 { get; private set; }
// Write only! - the MoonSharpVisible makes the getter hidden and the setter visible!
public int Property4 { [MoonSharpVisible(false)] get; [MoonSharpVisible(true)] private set; }
// Write only! - the MoonSharpVisible makes the whole property hidden but another attribute resets the setter as visible!
[MoonSharpVisible(false)]
public int Property5 { get; [MoonSharpVisible(true)] private set; }
// Not visible at all - the MoonSharpVisible hides everything
[MoonSharpVisible(false)]
public int Property6 { get; set; }
// Not visible - it's private
private event EventHandler Event1;
// Visible - it's public
public event EventHandler Event2;
// Visible - it's private but forced visible by attribute
[MoonSharpVisible(true)]
private event EventHandler Event3;
// Not visible - it's public but forced hidden by attribute
[MoonSharpVisible(false)]
public event EventHandler Event4;
// Not visible - visibility modifiers over add and remove are not currently supported!
[MoonSharpVisible(false)]
public event EventHandler Event5 { [MoonSharpVisible(true)] add { } [MoonSharpVisible(true)] remove { } }
} {% endhighlight %}
Sometimes it's needed to remove members from a registered type to hide them from scripts. There are several ways of doing this. One is to remove them manually after registration of the type:
{% highlight csharp %} var descr = ((StandardUserDataDescriptor)(UserData.RegisterType())); descr.RemoveMember("SomeMember"); {% endhighlight %}
Otherwise, simply add this attribute to the type declaration:
{% highlight csharp %} [MoonSharpHide("SomeMember")] public class SomeType ... {% endhighlight %}
This is pretty important as you might wish to hide, for example, inherited members you don't override.