C# から Lua スクリプトを実行する

NLua パッケージを利用することによって、 C# のプログラム中から Lua スクリプトを呼び出すことができる。

NLua パッケージの追加

nuget に NLua パッケージが存在するので、プロジェクトに追加する。
https://www.nuget.org/packages/NLua

$ dotnet add package NLua

最小のサンプル

// Program.cs
using NLua;

using var lua = new Lua();
lua.DoString("print('Hello,world')");   

動的にスクリプトを生成したり、テンプレートエンジンの生成結果を利用したりするなら、 DoString メソッドで手軽に動作させることができる。
外部ファイルから読み込んで実行したいときは、自前でファイル読み込みをせずに、次の方法を使うとよい。

ファイルから読み込んで実行

-- scrpit.lua
print('Hello,world')
// Program.cs
using NLua;

using Lua lua = new Lua();
lua.DoFile("script.lua");

外部の Lua ファイルを読み込む場合、C# プログラムのコンパイル後であっても Lua スクリプトを変更するだけでプログラムをカスタマイズすることができる。

C# 側から Lua の関数を呼び出す

-- script.lua
function add_and_sub(a, b)
    print("add_and_sub called")
    return a + b, a - b
end
// Program.cs
using NLua;

using Lua lua = new();
lua.DoFile("script.lua");

LuaFunction? add_and_sub = lua["add_and_sub"] as LuaFunction;
if (foo != null)
{
    object[] result = add_and_sub.Call(1, 2);
    Console.WriteLine("foo result add: " + result[0]);
    Console.WriteLine("foo result sub: " + result[1]);
}

Lua オブジェクトはインデクサを使って大域オブジェクトにアクセスできるので、ラッパー型である LuaFunction の Call メソッドを使って呼び出すことができる。

引数および戻り値はともに object[] である。
(Call に渡す実引数について、足りない値は nil で埋められ、余分な値は無視される。)
(Call の戻り値は配列である。Lua の関数は複数の値を返せるので、戻り値を利用するときは何番目を使うか指定する。戻り値が 1 つしかない場合でも同様。)

.NET オブジェクトの共有

.NET のオブジェクトインスタンスを Lua スクリプトと共有することができる模様。

public class SampleClass
{
    public int x;
    public int y;
}

class App
{
    static void Main(string[] args)
    {
        using Lua lua = new();
        
        SampleClass sample = new();
        sample.x = 10;
        sample.y = 20;
        lua["sample"] = sample;
        
        Console.WriteLine("before: x = " + sample.x + " y = " + sample.y);
        
        lua.DoString("""
            function overwrite()
                sample.x = 99
                sample.y = 99
            end
        """);

        (lua["overwrite"] as LuaFunction)!.Call();
        
        Console.WriteLine("after : x = " + sample.x + " y = " + sample.y);
    }
}
before: x = 10 y = 20
after : x = 99 y = 99

なお、共有したオブジェクトを Lua のテーブルで書き換えてしまうと、C# 側の元の参照からはそれが見えないので注意が必要。

using Lua lua = new();

SampleClass sample = new();
sample.x = 10;
sample.y = 20;
lua["sample"] = sample;

var before = lua.GetObjectFromPath("sample");
Console.WriteLine("type : " + before.GetType().Namespace + "." + before.GetType().Name);
Console.WriteLine("C#  before: x = " + sample.x + " y = " + sample.y);

lua.DoString("""
    function overwrite()
        print("Lua before: x = " .. sample.x .. " y = " .. sample.y)
        sample = {
            x = 99,
            y = 99
        }
        print("Lua after : x = " .. sample.x .. " y = " .. sample.y)
    end
""");
(lua["overwrite"] as LuaFunction)!.Call();

Console.WriteLine("C#  after : x = " + sample.x + " y = " + sample.y);

var after = lua.GetObjectFromPath("sample");
Console.WriteLine("type : " + after.GetType().Namespace + "." + after.GetType().Name);
type : LuaApp.SampleClass
C#  before: x = 10 y = 20
Lua before: x = 10 y = 20
Lua after : x = 99 y = 99
C#  after : x = 10 y = 20
type : NLua.LuaTable

Lua 側から C# の関数を呼び出す

インスタンスメソッド

Lua らしい記法を利用して C# 側のメソッドを透過的に呼び出すことができる。

public class SampleClass
{
    public int x;
    public int y;
    
    public void add(int x, int y)
    {
        this.x += x;
        this.y += y;
    }
}

class App
{
    static void Main(string[] args)
    {
        using Lua lua = new();
        
        SampleClass sample = new();
        sample.x = 10;
        sample.y = 20;
        
        lua["sample"] = sample;
        lua.DoString("""
            function call_add()
                sample:add(5, 5)
            end
        """);
        
        (lua["call_add"] as LuaFunction)!.Call();
        System.Console.WriteLine("x: " + sample.x + " y: " + sample.y);
    }
}
x: 15 y: 25

クラスメソッド

static メソッドをインスタンス経由で呼び出すことはできないので、 CLR (共通言語ランタイム) のパッケージとして対象のクラスをロードしてから呼び出す。
Lua 内の import(assembly_name, namespace_name) は、 LoadCLRPackage() メソッドを呼び出すことによって利用可能になる関数。

public class SampleClass
{
    public static void Greet(string name, int age) {
        string greet = "Hello " + name + ", " + age + " years old";
        Console.WriteLine(greet);
    }
}

class App
{
    static void Main(string[] args)
    {
        using Lua lua = new();
        lua.LoadCLRPackage();
        
        lua.DoString("""
            import('nlua_example', 'LuaApp')
            function greet()
                SampleClass.Greet('Lua', 20)
            end
        """);
        
        (lua["greet"] as LuaFunction)!.Call();
    }
}

関数

C# 側の処理を関数として Lua 側に公開したいなら、関数自体をそのまま共有することによって実現できる。

static void Main(string[] args)
{
    using Lua lua = new();
    
    lua["greet"] = (Func<string, int, string>)Greet;
    
    lua.DoString("""
        greet('Lua', 10);
    """);   
}

static string Greet(string name, int age)
{
    string greet = "Hello " + name + ", " + age + " years old";
    Console.WriteLine(greet);
    return greet;
}