サーバースクリプトに使える言語の比較
1.5.8.1 のプリザンターでは、サーバースクリプトは ClearScript(V8)で動く JavaScript だけです。このページは、JavaScript 以外の言語を足す場合にどれが現実的かを比べた設計メモで、本体の標準機能ではありません。調査は 1.5.1.0 を対象に行いました。
Python は サーバースクリプトを Python で書けるようにする、C# スクリプトは C# スクリプト(Roslyn)をサーバースクリプトに使う にあります。
評価の基準
図を読み込み中…
| 基準 | 内容 |
|---|---|
| ホストオブジェクトを渡せる | AddHostObject 相当で context・model・items などを渡せる |
| OS 操作を遮断できる | ファイル I/O・プロセス実行・直接のネットワーク接続ができない |
| タイムアウトを制御できる | 無限ループを ContinuationCallback 相当で止められる |
| 依存が NuGet だけ | ネイティブ DLL や外部プロセスを必須にしない |
| 後方互換 | 既存の JavaScript のスクリプトに影響しない |
結論
| 言語 | 実行方式 | サンドボックス | 評価 |
|---|---|---|---|
| TypeScript | JavaScript に変換して既存の V8 で実行 | V8 と同じ(原理的保証) | 推奨 |
| Lua(MoonSharp) | 純粋 C# の Lua VM | 設計上サンドボックス前提 | 中優先 |
| Python(IronPython) | DLR | 4 層で封鎖(実装的保証) | 別ページ |
| C# スクリプト(Roslyn) | .NET CLR | 検査のみ(不完全) | 条件付き |
| F# Scripting | .NET CLR | C# スクリプトと同じ課題 | 低優先 |
| Ruby(MRuby) | ネイティブ VM を P/Invoke | ビルド時に標準ライブラリを外せる | 低優先 |
| PHP(PeachPie) | PHP を IL にコンパイル | C# スクリプトと同じ課題 | 不推奨 |
| PowerShell | PowerShell SDK | OS 操作が前提の言語 | 不可 |
| Groovy・Kotlin Script(IKVM.NET) | JVM を .NET 上で模倣 | Java の SecurityManager が廃止済み | 不可 |
| VBScript・VB.NET Script | COM・Roslyn | .NET 10 非対応・C# と同じ課題 | 不可 |
| 言語 | 追加の依存 | 実装コスト |
|---|---|---|
| TypeScript | typescript.js を埋め込むだけ | 低 |
| Lua | MoonSharp(約 1 MB) | 中 |
| Python | IronPython 3.4.x | 高 |
| Ruby | 公式 NuGet なし・libmruby をプラットフォーム別にビルド | 高 |
| PHP | Peachpie.Runtime など | 高 |
| C#・F# | Microsoft.CodeAnalysis.CSharp.Scripting・FSharp.Compiler.Service | 高 |
| Groovy・Kotlin | IKVM と jar(合計 55 MB 以上) | 非常に高 |
進める順は、IScriptEngine の抽出 → TypeScript → Python → Lua → その他(MRuby など)です。TypeScript は抽出さえ済めば新しいエンジンが要らず、安全性も変わらないので、Python より先に着手できます。
TypeScript
TypeScript は JavaScript のスーパーセットで、型注釈は JavaScript への変換時にすべて消えます。変換後の JavaScript を既存の V8 で実行するので、セキュリティモデルは今と同じで、JavaScript でできないことは TypeScript でもできません。既存の JavaScript はそのまま TypeScript として通り、型定義ファイルを配れば VS Code の補完・型チェックが使えます。
変換方式
| 方式 | 内容 | 評価 |
|---|---|---|
| A:実行のたびに V8 内で変換 | リクエストごとに typescript.js(minify 版で約 6 MB)を読み込む | 見送り。現行はリクエストごとにエンジンを作って破棄する(ServerScriptUtilities.cs#L1217)ため、毎回数百ミリ秒かかる |
| B:変換専用の V8 を 1 つ持ち、結果をキャッシュ | 保存時か初回実行時に変換し、実行時はキャッシュした JavaScript を通常の V8 で動かす | 採用 |
| C:Node.js のプロセスで変換 | Jering.Javascript.NodeJS など | 見送り。Node.js のインストールが要る |
図を読み込み中…
TypeScriptTranspilerService の要点
- 新規ファイル
Libraries/ServerScripts/TypeScriptTranspilerService.cs。Lazy<T>のシングルトンで、専用のV8ScriptEngineに埋め込みリソースのtypescript.min.jsを 1 回だけ読み込む。 ts.transpileModule()をtarget: ES2020・module: None・strict: false・reportDiagnostics: trueで呼ぶ。module: Noneなのでimport・exportは構文エラーになる。デコレータのメタデータ出力(emitDecoratorMetadata)は無効にする。- 結果はソースの SHA-256 をキーに
ConcurrentDictionaryに入れる。変換用の V8 はスレッドセーフではないので、変換だけlockし、キャッシュに当たったときはロックしない。スクリプトの更新時にキャッシュを消す。 typescript.min.jsは npm のtypescriptパッケージから取り、.csprojにEmbeddedResourceとして追加する。新しい NuGet パッケージは要らない。
本体の変更
ServerScriptのLanguage(Python 版 で追加するint?)にTypeScript = 2を足す。ServerScriptUtilities.Executeで、TypeScript のスクリプトはTypeScriptTranspilerService.Instance.Transpile(script.Body)を通してから V8 に渡す。ホストオブジェクトの登録は変えない。- ダイアログの言語の選択に
TypeScriptを足し、コードエディタのモードをtypescriptにする。
// TypeScript のサーバースクリプトの例
const title: string = model.Title as string;
if ((context.UserId as number) === 0) {
context.ErrorData.Type = 1;
}
model.ClassA = `処理済み: ${title}`;型定義ファイル(declare const context: { readonly UserId: number; ... } など)は補完・型チェック専用で、実行時には使いません。本体には入れず、npm パッケージや VS Code 拡張、ダイアログからのダウンロードで配ります。
| 項目 | TypeScript | JavaScript(現行) |
|---|---|---|
| 変換サービスの初期化 | 1 回だけ約 500 ミリ秒 | なし |
| 初回の実行 | 変換 約 10〜50 ミリ秒 + V8 | V8 のみ |
| 2 回目以降 | キャッシュ 1 ミリ秒未満 + V8 | V8 のみ |
ts.transpileModule() は型チェックをせず構文の変換だけなので、tsc のフルコンパイルよりずっと速く、100 行程度で 5〜20 ミリ秒です。構文エラーは reportDiagnostics の結果をユーザーに見せます。
Lua(MoonSharp)
MoonSharp は純粋 C# の Lua 実装(MIT、約 1 MB)で、ゲームの MOD 用に作られたためサンドボックスが設計の中心にあります。Lua VM は OS の API を持たず、io・os モジュールは組み込まないようにでき、.NET のオブジェクトは UserData.RegisterType<T>() で明示的に登録したものだけが見えます。タイムアウトも制御できます。
IronPython より構造的に安全で軽量です。ExpandoObject は IDictionary 経由になるのでラッパーが要り、既存の JavaScript とは互換がありません。
その他の言語
| 言語 | 評価の理由 |
|---|---|
| PowerShell | システム管理のための言語で、Get-Content・Start-Process・Invoke-WebRequest・[System.IO.File]::ReadAllText() などが中核機能。ConstrainedLanguage モードでも基本のコマンドレットは使え、完全な制御には OS レベルの AppLocker・WDAC が要る。NoLanguage にすると変数の代入もできない |
| F# Scripting | .NET CLR で直接動くので C# スクリプトと同じ課題。ExpandoObject へのアクセスに ? 演算子の拡張などが要り、フォームの値操作という用途と関数型の相性もよくない |
| VBScript | WSH(COM)ベースで .NET 10 では動かず、Microsoft が廃止を予告している |
| VB.NET Script | Roslyn 上で C# スクリプトと同じ課題があり、利用者も少ない |
| Ruby(MRuby) | IronRuby は 2011 年の 1.1.3 で止まり .NET 10 で動かないので、候補は C 実装の MRuby だけ。File・Process をビルド時に外せる点は優れるが、公式の NuGet が無く、プラットフォーム別のネイティブ DLL、P/Invoke と GCHandle によるホストオブジェクトの受け渡し、eval・require の制限が要る |
| PHP(PeachPie) | PHP を IL にコンパイルして CLR で動かすので C# スクリプトと同じ課題に加え、file_get_contents・exec などの PHP 関数も使える。disable_functions 相当の制御はランタイムでは難しく、プロジェクトの更新も散発的 |
| Groovy・Kotlin Script | IKVM.NET で JVM を模倣する。Java の SecurityManager は Java 17 で非推奨、Java 21 で削除されており、サンドボックスを作る手段が無い。依存は 55 MB 以上で、.NET 10 での動作も確認されていない |