Calling C# from Lua
đĄ PuerTS 3.0 also supports calling C# from JavaScript and Python. Each language has its own syntax â click the links to see the corresponding tutorials.
In the previous tutorial, we ran a simple Hello World:
void Start() {
var env = new Puerts.ScriptEnv(new Puerts.BackendLua());
env.Eval(@"print('hello world')");
env.Dispose();
}
With PuerTS, the integration between Lua and C# goes much further. Read on!
Accessing C# Namespacesâ
To access C# types from Lua, you first need to get the C# namespace entry via require('csharp'):
local CS = require('csharp')
-- Now you can access any C# type through CS
-- e.g.: CS.UnityEngine.Debug, CS.System.Collections.Generic.List_1
The CS object is the gateway to the C# world. You can access any C# type by providing its FullName (the full namespace path).
Of course, typing the full namespace every time is tedious. You can simplify it by assigning to a local variable:
local Vector2 = CS.UnityEngine.Vector2
print(Vector2.one)
Object Creationâ
void Start() {
var env = new Puerts.ScriptEnv(new Puerts.BackendLua());
env.Eval(@"
local CS = require('csharp')
local v = CS.UnityEngine.Vector3(1, 2, 3)
print(tostring(v))
-- (1.0, 2.0, 3.0)
");
env.Dispose();
}
In this example, we created a C# Vector3 directly from Lua!
â ī¸ Note: Lua does not use the
newkeyword to create C# objects. Simply call the type as a function. This differs from JS where you usenew CS.xxx().
Property Access and Method Callsâ
Once an object is created, calling methods and accessing properties is straightforward.
void Start() {
var env = new Puerts.ScriptEnv(new Puerts.BackendLua());
env.Eval(@"
local CS = require('csharp')
-- Static method call: use dot syntax
CS.UnityEngine.Debug.Log('Hello World')
-- Create an object and call instance methods
local rect = CS.UnityEngine.Rect(0, 0, 2, 2)
CS.UnityEngine.Debug.Log(rect:Contains(CS.UnityEngine.Vector2.one)) -- True
rect.width = 0.1
CS.UnityEngine.Debug.Log(rect:Contains(CS.UnityEngine.Vector2.one)) -- False
");
env.Dispose();
}
â ī¸ Key syntax difference: In Lua, instance methods use colon
:syntax (e.g.,obj:Method()), while static methods and properties use dot.syntax (e.g.,Class.Method()). The colon syntax automatically passes the object itself as the first argument.
-- Instance method â use colon
local testHelper = CS.Puerts.UnitTest.TestHelper.GetInstance()
testHelper:NumberTestPipeLine(1, outRef, callback)
-- Static method â use dot
CS.Puerts.UnitTest.TestHelper.AssertAndPrint('msg', 1, 1)
-- Property read/write â use dot
testHelper.numberTestField = 3 -- write instance property
local val = testHelper.numberTestField -- read instance property
CS.Puerts.UnitTest.TestHelper.numberTestFieldStatic = 3 -- write static property
ref/out Parametersâ
C# ref and out parameters are handled in Lua using tables as containers:
// C# side
class Example {
public static double InOutArgFunc(int a, out int b, ref int c)
{
Debug.Log("a=" + a + ",c=" + c);
b = 100;
c = c * 2;
return a + b;
}
}
void Start() {
var env = new Puerts.ScriptEnv(new Puerts.BackendLua());
env.Eval(@"
local CS = require('csharp')
-- Create tables as out/ref containers
local outB = {} -- out parameter: empty table
local refC = {}
refC[1] = 10 -- ref parameter: initial value at table[1]
local ret = CS.Example.InOutArgFunc(100, outB, refC)
print('ret=' .. ret .. ', out=' .. outB[1] .. ', ref=' .. refC[1])
-- ret=200, out=100, ref=20
");
env.Dispose();
}
đ Rules:
outparameter: pass an empty table{}, the result is stored attable[1]after the callrefparameter: pass a table with the initial value attable[1], the updated value is attable[1]after the call
Genericsâ
C# generic types in Lua require puerts.generic() to create:
void Start() {
var env = new Puerts.ScriptEnv(new Puerts.BackendLua());
env.Eval(@"
local CS = require('csharp')
local puerts = require('puerts')
-- Create List<int> type
local List = puerts.generic(CS.System.Collections.Generic.List_1, CS.System.Int32)
local ls = List()
ls:Add(1)
ls:Add(2)
ls:Add(3)
");
env.Dispose();
}
â ī¸ Note the generic type naming convention: C# backtick notation (e.g.,
List`1) is represented as underscores in Lua (e.g.,List_1). Common mappings:
List<T>âList_1Dictionary<TKey, TValue>âDictionary_2Action<T>âAction_1
Generic Methodsâ
To call generic methods, use puerts.genericMethod():
void Start() {
var env = new Puerts.ScriptEnv(new Puerts.BackendLua());
env.Eval(@"
local CS = require('csharp')
local puerts = require('puerts')
-- Call static generic method GenericTestClass.StaticGenericMethod<int>()
local func = puerts.genericMethod(
CS.Puerts.UnitTest.GenericTestClass, -- type
'StaticGenericMethod', -- method name
CS.System.Int32 -- generic type argument
)
print(func()) -- 'Int32'
-- Call instance generic method
local testobj = CS.Puerts.UnitTest.GenericTestClass()
testobj.stringProp = 'world'
local instanceFunc = puerts.genericMethod(
CS.Puerts.UnitTest.GenericTestClass,
'InstanceGenericMethod',
CS.System.Int32
)
print(instanceFunc(testobj)) -- 'world_Int32'
");
env.Dispose();
}
đ
puerts.genericMethod()parameters: first is the type, second is the method name string, followed by generic type arguments. When calling an instance generic method, pass the object instance as the first argument.
Accessing Static Members and Nested Types of Generic Classesâ
local CS = require('csharp')
local puerts = require('puerts')
-- Create generic class GenericTestClass<string>
local GenericTestClass = puerts.generic(CS.Puerts.UnitTest.GenericTestClass_1, CS.System.String)
GenericTestClass.v = '6'
-- Access nested types
GenericTestClass.Inner()
print(GenericTestClass.Inner.stringProp) -- 'hello'
typeofâ
Since C#'s typeof cannot be accessed through the namespace, PuerTS provides the built-in puerts.typeof():
void Start() {
var env = new Puerts.ScriptEnv(new Puerts.BackendLua());
env.Eval(@"
local CS = require('csharp')
local typeof = require('puerts').typeof
local go = CS.UnityEngine.GameObject('testObject')
go:AddComponent(typeof(CS.UnityEngine.ParticleSystem))
-- Type comparison
local helper = CS.Puerts.UnitTest.CrossLangTestHelper()
local val = helper:GetDateTime()
print(val:GetType() == typeof(CS.System.DateTime)) -- true
");
env.Dispose();
}
Enumsâ
C# enum values can be accessed directly through the namespace path:
void Start() {
var env = new Puerts.ScriptEnv(new Puerts.BackendLua());
env.Eval(@"
local CS = require('csharp')
local helper = CS.Puerts.UnitTest.CrossLangTestHelper()
-- Read enum value
local enumVal = helper.EnumField
print(tostring(enumVal)) -- '213'
-- Set enum value
helper.EnumField = CS.Puerts.UnitTest.TestEnum.A
print(tostring(helper.EnumField)) -- '1'
");
env.Dispose();
}
Operator Overloadingâ
Since Lua's metatable-based operator overloading mechanism differs from C#, you need to use the op_Xxxx method names to invoke C# operator overloads:
void Start() {
var env = new Puerts.ScriptEnv(new Puerts.BackendLua());
env.Eval(@"
local CS = require('csharp')
local Vector3 = CS.UnityEngine.Vector3
-- C#: Vector3.up * 1600
local ret = Vector3.op_Multiply(Vector3.up, 1600)
print(tostring(ret)) -- (0.0, 1600.0, 0.0)
");
env.Dispose();
}
đ Common operator mappings:
+âop_Addition-âop_Subtraction*âop_Multiply/âop_Division==âop_Equality
Passing nullâ
In Lua, use nil to represent C#'s null:
local CS = require('csharp')
local testHelper = CS.Puerts.UnitTest.TestHelper.GetInstance()
testHelper:PassStr(nil) -- pass null string
testHelper:PassObj(nil) -- pass null object
For nullable structs (Nullable<T>), also use nil:
local outRef = {}
outRef[1] = nil
testHelper:NullableNativeStructTestPipeLine(nil, outRef, function(obj)
print(obj == nil) -- true
return nil
end)
This covers calling C# from Lua. In the next part, we'll look at the reverse: Invoking Lua from C#.
đ Calling C# from other languages: JS to C# | Python to C# | Multi-Language Comparison Cheat Sheet