Bicameral Engine
GPU-resident engine for a magic-research open-world game
Loading...
Searching...
No Matches
luau_sandbox.h
Go to the documentation of this file.
1// luau_sandbox.h — Luau を CPU で安全に動かす殻(サンドボックス。T-0020、13 §2・D-318・D-320)。
2//
3// ゲームを作る人(ユーザー・仲間・Mod を作る人)が書く Luau は、全部この殻の中で動かす。
4// 見せるもの: 決まった標準ライブラリ(base の一部・coroutine・table・string・math・utf8・bit32・buffer・vector)
5// + ホストが Seal() の前に登録した関数だけ(1 つの表にまとめる。例: `host.emit(x)`)。
6// 見せないもの: ファイル・OS・時計・ネットワーク・require・debug・環境の書き換え(getfenv/setfenv)・メモリの量(gcinfo)。
7// 実行には上限がある: 安全点(ループの折り返し・関数の呼び出し)の数と、使ってよいメモリの量。超えたら止めて理由を返す。
8// 決定性: 同じ殻の設定・同じソース・同じ実行の順番なら、同じ結果になる(math.random は実行ごとに決まった種から。13 §2・ADR-0030)。
9// ただし表を「関数や表をキーにして」pairs で回す順番はアドレスで決まるので、ベイクの結果にその順番を使わない(13 §2)。
10// データの流れ: エディタ・ツール → Create → RegisterFunction(何個でも)→ Seal → Run(ソースごと)→ 戻り値の文字列・ホスト関数の副作用。
11// パッケージ(luau_package.*。T-0138)は Run に RunOptions を渡す: require をそのパッケージの中だけで見せ、戻り値を ScriptValue で受け取る。
12// ここは CPU だけ(GPU もシミュの状態も知らない。原則 1・2)。シミュに効かせるときは、ホスト関数が表やコマンドを作る(13 §1)。
13#pragma once
14
15#include <cstdint>
16#include <expected>
17#include <functional>
18#include <memory>
19#include <string>
20#include <string_view>
21#include <vector>
22
23#include "script/script_value.h"
24
25struct lua_State;
26
27namespace bicameral::script {
28
29 // ホストが Luau に見せる関数。引数と戻り値は Luau の C API(lua.h)で読み書きする。
30 // 登録した時の context は HostContext(state) で取り出せる
31 using HostFunction = int (*)(lua_State* state);
32
34 // --- 実行の上限(1 回の Run ごと)---
35 uint64_t safepointLimit = 50'000'000; // 安全点(ループの折り返し・呼び出し)の数。無限ループを止める
36 uint64_t memoryLimitBytes = 256ull << 20; // 殻全体で使ってよいメモリ(バイト)
37
38 // --- 決定性 ---
39 int32_t randomSeed = 0; // 各 Run の始めに math.randomseed(randomSeed) する
40
41 // --- 出力 ---
42 // print の行き先。空ならログ(Channel::Tool)へ
43 std::function<void(std::string_view chunkName, std::string_view text)> printSink;
44 };
45
46 enum class ScriptErrorKind : uint8_t {
47 Compile, // 文法の誤り
48 Runtime, // 実行中の error()・型の誤りなど
49 SafepointLimit, // 安全点の上限を超えた(無限ループなど)
50 Memory, // メモリの上限を超えた
51 Usage, // 殻の使い方の誤り(Seal の前に Run した・Seal の後に登録した)
52 };
53
54 struct ScriptError {
56 std::string message; // "chunk:行: 内容" の形(Luau の書式)
57 };
58
59 // require で読むモジュール(パッケージの中のファイル。T-0138)
60 struct ModuleFile {
61 std::string chunkName; // エラーに出る名前で、同じ Run の中でのキャッシュの鍵(例 "core/sub/util.luau")
62 std::string source;
63 };
64
65 // require("名前") の名前をファイルにする。無い・許さない名前なら理由を返す(呼んだスクリプトに error として届く)
66 using ModuleResolver = std::function<std::expected<ModuleFile, std::string>(std::string_view name)>;
67
68 // Run の追加の指定(既定は T-0020 の殻のまま: require なし・戻り値は文字列だけ)
69 struct RunOptions {
70 const ModuleResolver*
71 modules = nullptr; // あれば、この Run の中だけ require が見える。1 つのモジュールは 1 回だけ走る
72 bool captureValues = false; // 戻り値を ScriptValue でも返す(表は中まで。関数などは error)
73 };
74
75 // 1 回の Run の結果
76 struct RunResult {
77 std::vector<std::string> returns; // チャンクが return した値を tostring したもの
78 std::vector<ScriptValue> values; // captureValues のときだけ: 戻り値の木
79 uint64_t safepointCount = 0; // 使った安全点の数(重さの目安・決定性の検査)
80 };
81
83 public:
84 [[nodiscard]] static std::expected<std::unique_ptr<LuauSandbox>, std::string> Create(SandboxLimits limits);
86
87 LuauSandbox(const LuauSandbox&) = delete;
89
90 // --- 組み立て(Seal の前だけ)---
91
92 // グローバルの表 tableName の name に関数を置く(表が無ければ作る)。context は関数の中で HostContext() で取り出す
93 [[nodiscard]] std::expected<void, ScriptError> RegisterFunction(const char* tableName, const char* name,
94 HostFunction function, void* context);
95
96 // グローバルと標準ライブラリを読み取り専用にする。これ以降は Run だけ
97 void Seal();
98
99 // --- 実行(Seal の後)---
100
101 // ソースをコンパイルして、新しいスレッド(グローバルは書き込める自分用の表。ほかの Run と混ざらない)で最後まで走らせる
102 [[nodiscard]] std::expected<RunResult, ScriptError> Run(std::string_view chunkName, std::string_view source,
103 const RunOptions& options = {});
104
105 // --- 状態 ---
106 [[nodiscard]] bool IsSealed() const { return m_sealed; }
107 [[nodiscard]] uint64_t MemoryBytes() const { return m_memoryBytes; }
108
109 // ホスト関数の中から、登録した時の context を取り出す
110 [[nodiscard]] static void* HostContext(lua_State* state);
111
112 private:
113 explicit LuauSandbox(SandboxLimits limits);
114
115 [[nodiscard]] std::expected<void, std::string> OpenLibraries();
116 void ReseedRandom();
117
118 static void* Allocate(void* userData, void* pointer, size_t oldSize, size_t newSize);
119 static void Interrupt(lua_State* state, int gc);
120 static int Print(lua_State* state);
121 static int Require(lua_State* state);
122
123 [[nodiscard]] std::expected<RunResult, ScriptError> RunThread(lua_State* thread, std::string_view chunkName,
124 const char* bytecode, size_t bytecodeSize,
125 const RunOptions& options);
126 [[nodiscard]] std::expected<RunResult, ScriptError> CollectResults(lua_State* thread,
127 std::string_view chunkName,
128 const RunOptions& options);
129
130 void BeginModules(lua_State* thread, const ModuleResolver* modules);
131 void EndModules();
132 [[nodiscard]] std::expected<void, std::string> LoadModule(lua_State* state, std::string_view name);
133 [[nodiscard]] std::expected<void, std::string> RunModule(lua_State* state, const std::string& chunkName,
134 std::string_view source);
135
137 lua_State* m_state = nullptr;
138 bool m_sealed = false;
139
140 // --- 上限の数え方 ---
141 uint64_t m_memoryBytes = 0;
142 uint64_t m_safepointCount = 0;
144 bool m_memoryExceeded = false;
145 std::string m_currentChunk; // print の行き先に渡す名前
146
147 // --- require(Run の間だけ)---
148 const ModuleResolver* m_modules = nullptr;
149 int m_moduleCacheRef = 0; // chunkName → モジュールの戻り値 の表(Luau の registry)
150 std::vector<std::string> m_moduleLoading; // 読み込み中の chunkName(循環を見つける)
151 };
152
153} // namespace bicameral::script
std::expected< RunResult, ScriptError > Run(std::string_view chunkName, std::string_view source, const RunOptions &options={})
void BeginModules(lua_State *thread, const ModuleResolver *modules)
std::expected< void, ScriptError > RegisterFunction(const char *tableName, const char *name, HostFunction function, void *context)
std::expected< RunResult, ScriptError > CollectResults(lua_State *thread, std::string_view chunkName, const RunOptions &options)
static void Interrupt(lua_State *state, int gc)
std::expected< void, std::string > OpenLibraries()
static int Require(lua_State *state)
std::expected< void, std::string > RunModule(lua_State *state, const std::string &chunkName, std::string_view source)
LuauSandbox & operator=(const LuauSandbox &)=delete
std::expected< void, std::string > LoadModule(lua_State *state, std::string_view name)
LuauSandbox(const LuauSandbox &)=delete
static std::expected< std::unique_ptr< LuauSandbox >, std::string > Create(SandboxLimits limits)
std::expected< RunResult, ScriptError > RunThread(lua_State *thread, std::string_view chunkName, const char *bytecode, size_t bytecodeSize, const RunOptions &options)
static void * Allocate(void *userData, void *pointer, size_t oldSize, size_t newSize)
const ModuleResolver * m_modules
std::vector< std::string > m_moduleLoading
static void * HostContext(lua_State *state)
static int Print(lua_State *state)
std::function< std::expected< ModuleFile, std::string >(std::string_view name)> ModuleResolver
int(*)(lua_State *state) HostFunction
const ModuleResolver * modules
std::vector< std::string > returns
std::vector< ScriptValue > values
std::function< void(std::string_view chunkName, std::string_view text)> printSink