High performance Lua interpreter implemented in C# for .NET and Unity
English | 日本語
Lua-CSharpはC#実装のLuaインタプリタを提供するライブラリです。Lua-CSharpを導入することで、.NETアプリケーション内で簡単にLuaスクリプトを組み込むことが可能になります。
Lua-CSharpは最新のC#の機能を活用し、低アロケーション・ハイパフォーマンスを念頭において設計されています。Lua-CSharpはC#アプリケーションに組み込まれることを前提としているため、C#-Lua間の相互運用時に最大のパフォーマンスを発揮するように最適化されています。以下はMoonSharp, NLuaと比較したベンチマークです。
MoonSharpは多くの場合で十分な速度を発揮しますが、設計上非常に大きいアロケーションが発生します。NLuaはCバインディングであるため動作そのものは高速ですが、C#レイヤーとのやり取りの際に大きなオーバーヘッドがかかります。Lua-CSharpは完全にC#で実装されているため、C#コードとオーバーヘッドなしでやり取りが可能です。また、IL生成などを一切使用しないためAOT環境でも安定して動作します。
Lua-CSharpを利用するには.NET Standard2.1以上が必要です。パッケージはNuGetから入手できます。
dotnet add package LuaCSharp
Install-Package LuaCSharp
Lua-CSharpをUnityで利用することも可能です。詳細はLua.Unityの項目を参照してください。
LuaStateクラスを利用することでC#上からLuaスクリプトを実行することが可能です。以下はLuaで記述された簡単な演算を評価するサンプルコードです。
using Lua;
// LuaStateを作成する
var state = LuaState.Create();
// DoStringAsyncで文字列のLuaスクリプトを実行する
var results = await state.DoStringAsync("return 1 + 1");
// 2
Console.WriteLine(results[0]);
[!WARNING]
LuaStateはスレッドセーフではありません。同時に複数のスレッドからアクセスしないでください。
Luaスクリプト上の値はLuaValue型で表現されます。LuaValueの値はTryRead<T>(out T value)またはRead<T>()で読み取ることが可能です。
var results = await state.DoStringAsync("return 1 + 1");
// double
var value = results[0].Read<double>();
また、Typeプロパティから値の型を取得できます。
var isNil = results[0].Type == LuaValueType.Nil;
Lua-C#間の型の対応を以下に示します。
| Lua | C# |
|---|---|
nil |
LuaValue.Nil |
boolean |
bool |
string |
string |
number |
double, float, int |
table |
LuaTable |
function |
LuaFunction |
(light)userdata |
object |
userdata |
ILuaUserData |
thread |
LuaState |
C#側からLuaValueを作成する際には、変換可能な型の場合であれば暗黙的にLuaValueに変換されます。
LuaValue value;
value = 1.2; // double -> LuaValue
value = "foo"; // string -> LuaValue
value = new LuaTable() // LuaTable -> LuaValue
LuaのテーブルはLuaTable型で表現されます。これは通常のLuaValue[]やDictionary<LuaValue, LuaValue>のように使用できます。
// Lua側でテーブルを作成
var results = await state.DoStringAsync("return { a = 1, b = 2, c = 3 }");
var table1 = results[0].Read<LuaTable>();
// 1
Console.WriteLine(table1["a"]);
// テーブルを作成
results = await state.DoStringAsync("return { 1, 2, 3 }");
var table2 = results[0].Read<LuaTable>();
// 1 (Luaの配列は1-originであることに注意)
Console.WriteLine(table2[1]);
state.EnvironmentからLuaのグローバル環境にアクセスできます。このテーブルを介して簡単にLua-C#間で値をやり取りすることが可能です。
// a = 10を設定
state.Environment["a"] = 10;
var results = await state.DoStringAsync("return a");
// 10
Console.WriteLine(results[0]);
Luaの標準ライブラリを利用することも可能です。state.OpenStandardLibraries()を呼び出すことで、LuaStateに標準ライブラリのテーブルを追加します。
using Lua;
using Lua.Standard;
var state = LuaState.Create();
// 標準ライブラリを追加
state.OpenStandardLibraries();
var results = await state.DoStringAsync("return math.pi");
Console.WriteLine(results[0]); // 3.141592653589793
標準ライブラリについてはLua公式のマニュアルを参照してください。
[!WARNING] Lua-CSharpは標準ライブラリの全ての関数をサポートしているわけではありません。詳細は互換性の項目を参照してください。
Luaの関数はLuaFunction型で表現されます。LuaFunctionによってLuaの関数をC#側から呼び出したり、C#で定義した関数をLua側から呼び出したりすることが可能です。
-- lua2cs.lua
local function add(a, b)
return a + b
end
return add;
var results = await state.DoFileAsync("lua2cs.lua");
var func = results[0];
// 引数を与えて関数を実行する
var funcResults = await state.CallAsync(func, [1, 2]);
// 3
Console.WriteLine(funcResults[0]);
配列のアロケーションが気になる場合は
var func = results[0];
var basePos = state.Stack.Count;
state.Push(func);
state.Push(1);
state.Push(2);
var funcResultsCount = await state.CallAsync(funcIndex:basePos, returnBase: basePos);
using (var reader = state.ReadStack(funcResultsCount))
{
var span = reader.AsSpan();
for (int i = 0; i < span.Length; i++)
{
Console.WriteLine(span[i]);
}
}
ラムダ式からLuaFunctionを作成することが可能です。
// グローバル環境に関数を追加
state.Environment["add"] = new LuaFunction((context, ct) =>
{
// context.GetArgument<T>()で引数を取得
var arg0 = context.GetArgument<double>(0);
var arg1 = context.GetArgument<double>(1);
// contextに戻り値を渡す
context.Return(arg0 + arg1);
// 複数なら以下のようにまとめて渡す必要がある
// context.Return(arg0, arg1);
// context.Return([arg0, arg1]);
// 戻り値の数を返す
return new(1);
// return new(context.Return(arg0 + arg1)); //でも可
});
// Luaスクリプトを実行
var results = await state.DoFileAsync("cs2lua.lua");
// 3
Console.WriteLine(results[i]);
-- cs2lua.lua
return add(1, 2)
[!TIP]
LuaFunctionによる関数の定義はやや記述量が多いため、関数をまとめて追加する際には[LuaObject]属性によるSource Generatorの使用を推奨します。詳細はLuaObjectの項目を参照してください。
通常の関数呼出し以外でもC#から様々なLuaの操作を行えます。
await state.CallAsync(func, [arg1, arg2]); // func(arg1,arg2) in lua
await state.AddAsync(arg1, arg2); // arg1 + arg2 in lua
await state.SubAsync(arg1, arg2); // arg1 - arg2 in lua
await state.MulAsync(arg1, arg2); // arg1 * arg2 in lua
await state.DivAsync(arg1, arg2); // arg1 / arg2 in lua
await state.ModAsync(arg1, arg2); // arg1 % arg2 in lua
await state.EqualsAsync(arg1, arg2); // arg1 == arg2 in lua
await state.LessThanAsync(arg1, arg2); // arg1 < arg2 in lua
await state.LessThanOrEqualsAsync(arg1, arg2); // arg1 <= arg2 in lua
await state.ConcatAsync([arg1, arg2, arg3]); // arg1 .. arg2 .. arg3 in lua
await state.GetTableAsync(table, key); // table[key] in lua
await state.GetTableAsync(table, "x"); // table.x in lua
await state.SetTableAsync(table, key, value); // table[key] = value in lua
await state.SetTableAsync(table, "x", value); // table.x = value in lua
LuaFunctionは非同期メソッドとして動作します。そのため、以下のような関数を定義することでLua側から処理の待機を行うことも可能です。
// 与えられた秒数だけTask.Delayで待機する関数を定義
state.Environment["wait"] = new LuaFunction(async (context, ct) =>
{
var sec = context.GetArgument<double>(0);
await Task.Delay(TimeSpan.FromSeconds(sec));
return context.Return();
});
await state.DoFileAsync("sample.lua");
-- sample.lua
print "hello!"
wait(1.0) -- 1秒待機する
print "how are you?"
wait(1.0) -- 1秒待機する
print "goodbye!"
このコードは以下の図のように、awaitで待機した後にLuaスクリプトの実行を再開することができます。これはゲームに組み込むスクリプトを記述する際などに非常に有用です。
LuaのコルーチンはLuaState型で表現されます。
コルーチンはLuaスクリプト内で利用できるだけでなく、Luaで作成したコルーチンをC#で待機することも可能です。
-- coroutine.lua
local co = coroutine.create(function()
for i = 1, 10 do
print("lua:", coroutine.yield(i))
end
end)
return co
var results = await state.DoFileAsync("coroutine.lua");
var co = results[0].Read<LuaThread>();
var stack = new LuaStack();
for (int i = 0; i < 10; i++)
{
var resumeResultsCount = await co.ResumeAsync(stack);
// coroutine.resume()と同様、成功時は最初の要素にtrue、それ以降に関数の戻り値を返す
// 1, 2, 3, 4, ...
if (resumeResultsCount > 1)
{
Console.WriteLine(stack[1]);
}
stack.Clear();
stack.Push(i);
}
[LuaObject]属性を用いることで、Lua上で動作する独自のクラスを作成することが可能になります。Luaで使いたいクラスにこの属性を付加することで、Source GeneratorがLua側から利用されるコードを自動生成します。
以下のサンプルコードはLuaで動作するSystem.Numerics.Vector3のラッパークラスの実装例です。
using System.Numerics;
using Lua;
var state = LuaState.Create();
// 定義したLuaObjectのインスタンスをグローバル変数として追加する
// (LuaObjectを追加したクラスにはLuaValueへの暗黙的変換が自動で定義される)
state.Environment["Vector3"] = new LuaVector3();
await state.DoFileAsync("vector3_sample.lua");
// LuaObject属性とpartialキーワードを追加
[LuaObject]
public partial class LuaVector3
{
Vector3 vector;
// Lua側で使用されるメンバーにLuaMember属性を付加
// 引数にはLuaでの名前を指定 (省略した場合はメンバーの名前が使用される)
[LuaMember("x")]
public float X
{
get => vector.X;
set => vector = vector with { X = value };
}
[LuaMember("y")]
public float Y
{
get => vector.Y;
set => vector = vector with { Y = value };
}
[LuaMember("z")]
public float Z
{
get => vector.Z;
set => vector = vector with { Z = value };
}
// staticメソッドの場合は通常のLua関数として解釈される
[LuaMember("create")]
public static LuaVector3 Create(float x, float y, float z)
{
return new LuaVector3()
{
vector = new Vector3(x, y, z)
};
}
// インスタンスメソッドの場合は暗黙的に自身のインスタンス(this)が1番目の引数として追加される
// これはLuaではinstance:method()のような表記でアクセスできる
[LuaMember("normalized")]
public LuaVector3 Normalized()
{
return new LuaVector3()
{
vector = Vector3.Normalize(vector)
};
}
}
-- vector3_sample.lua
local v1 = Vector3.create(1, 2, 3)
-- 1 2 3
print(v1.x, v1.y, v1.z)
local v2 = v1:normalized()
-- 0.26726123690605164 0.5345224738121033 0.8017836809158325
print(v2.x, v2.y, v2.z)
[LuaMember]を付加するフィールド/プロパティの型、またはメソッドの引数や戻り値の型はLuaValueまたはそれに変換が可能である必要があります。
ただし、戻り値にはvoid, Task/Task<T>, ValueTask/ValueTask<T>, UniTask/UniTask<T>, Awaitable/Awaitable<T>を利用することも可能です。
利用可能でない型に対してはSource Generatorがコンパイルエラーを出力します。
[LuaMetamethod]属性を追加することで、C#のメソッドをLua側で使用されるメタメソッドとして設定することが可能です。
例として、先ほどのLuaVector3クラスに__add, __sub, __tostringのメタメソッドを追加したコードを示します。
[LuaObject]
public partial class LuaVector3
{
// 上のコードで書かれた実装は省略
[LuaMetamethod(LuaObjectMetamethod.Add)]
public static LuaVector3 Add(LuaVector3 a, LuaVector3 b)
{
return new LuaVector3()
{
vector = a.vector + b.vector
};
}
[LuaMetamethod(LuaObjectMetamethod.Sub)]
public static LuaVector3 Sub(LuaVector3 a, LuaVector3 b)
{
return new LuaVector3()
{
vector = a.vector - b.vector
};
}
[LuaMetamethod(LuaObjectMetamethod.ToString)]
public override string ToString()
{
return vector.ToString();
}
}
local v1 = Vector3.create(1, 1, 1)
local v2 = Vector3.create(2, 2, 2)
print(v1) -- <1, 1, 1>
print(v2) -- <2, 2, 2>
print(v1 + v2) -- <3, 3, 3>
print(v1 - v2) -- <-1, -1, -1>
[!NOTE]
__index,__newindexは[LuaObject]の生成コードに利用されるため、設定することはできません。
Luaではrequire関数を用いてモジュールを読み込むことができます。通常のLuaではpackage.searchersの検索関数を用いてモジュールの管理を行いますが、Lua-CSharpではそれに加えてILuaModuleLoaderがモジュール読み込みの機構として提供されています。
これはpackage.searchersより先に実行されます。
public interface ILuaModuleLoader
{
bool Exists(string moduleName);
ValueTask<LuaModule> LoadAsync(string moduleName, CancellationToken cancellationToken = default);
}
これをLuaState.ModuleLoaderに設定することでモジュールの読み込み方法を変更することができます。デフォルトのLoaderにはluaファイルからモジュールをロードするFileModuleLoaderが設定されています。
また、CompositeModuleLoader.Create(loader1, loader2, ...)を利用することで複数のLoaderを組み合わせたLoaderを作成できます。
state.ModuleLoader = CompositeModuleLoader.Create(
new CustomModuleLoader1(),
new CustomModuleLoader2()
);
また、ロード済みのモジュールは通常のLua同様にpackage.loadedテーブルにキャッシュされます。これはLuaState.LoadedModulesからアクセスすることが可能です。
Lua-CSharpではサンドボックス化のために環境の抽象化をLuaPlatformとして提供しています。
var state = LuaState.Create(new LuaPlatform(FileSystem: new FileSystem(),
OsEnvironment: new SystemOsEnvironment(),
StandardIO: new ConsoleStandardIO(),
TimeProvider: TimeProvider.System));
ILuaFileSystem, ILuaOsEnvironment, ILuaStandardIO, ILuaStream
これらはrequire,print,dofileやosmoduleなどで使用されます。
Luaスクリプトの実行時例外はLuaRuntimeExceptionを継承した例外をスローします。これをcatchすることでエラー時の処理を行うことができます。
try
{
await state.DoFileAsync("filename.lua");
}
catch (LuaCompileException)
{
// 構文にエラーがあった際の処理
}
catch (LuaRuntimeException)
{
// 実行時例外が発生した際の処理
}
catch(OperationCanceledException)
{
// キャンセルが発生した際の処理
// LuaCanceledExceptionならlua内でのキャンセル位置を取得可能
}
Lua-CSharpはUnityで利用することも可能です。(Mono/IL2CPPの両方で動作します)
また、Lua-CSharpをUnityと統合するためのLua.Unity拡張パッケージも提供されています。
NugetForUnityをインストールします。
NuGet > Manage NuGet PackagesからNuGetウィンドウを開き、LuaCSharpパッケージを検索してインストールします。
Window > Package ManagerからPackage Managerウィンドウを開き、[+] > Add package from git URLから以下のURLを入力します。
https://github.com/nuskey8/Lua-CSharp.git?path=src/Lua.Unity/Assets/Lua.Unity
Lua.Unityを導入することで、.lua拡張子のファイルをLuaAssetとして扱えるようになります。
これは通常のTextAssetのように利用することが可能です。
var asset = Resources.Load<LuaAsset>("example");
await state.DoStringAsync(asset.Text, ct);
また、内部でResourcesまたはAddressablesを利用するILuaModuleLoaderの実装が用意されています。
// モジュールの読み込みにResourcesを利用する
state.ModuleLoader = new ResourcesModuleLoader();
// モジュールの読み込みにAddressablesを利用する (Addressablesパッケージが必要)
state.ModuleLoader = new AddressablesModuleLoader();
LuaPlatformの要素として
UnityStandardIO,
UnityApplicationOsEnvironmentが提供されています。
UnityStandardIOではprintなどがDebug.Logに出力されるようになります。
UnityApplicationOsEnvironmentでは環境変数を辞書で設定でき、os.exit()でApplication.Quit()が呼ばれるようになります。
あくまで簡易的なものなので、実際の運用ではあまり推奨されません。
Lua-CSharpは.NETとの統合を念頭に設計されているため、C実装とは互換性がない仕様がいくつか存在します。
Lua-CSharpで利用される文字コードはUTF-16です。通常Luaは1バイトで1文字を表すエンコーディングを前提としているため、文字列まわりの動作が大きく異なります。
例えば、以下のコードの出力結果は通常のLuaでは15ですが、Lua-CSharpでは5です。
local l = string.len("あいうえお")
print(l)
Stringライブラリの関数はすべて文字列をUTF-16として扱う実装に変更されていることに注意してください。
Lua-CSharpはC#で実装されているため.NETのGCに依存しています。そのため、メモリ管理に関する動作が通常のLuaとは異なります。
collectgarbage()は利用可能ですが、これは単にGC.Collect()の呼び出しです。引数の値は無視されます。また、弱参照テーブル(week tables)はサポートされません。
このライブラリはMITライセンスの下で提供されています。