载入中...
搜索中...
未找到
ScriptModule.cpp
浏览该文件的文档.
2
3#include "common/Capability.h"
5
6#include <squirrel.h>
7
8#include <algorithm>
9#include <fstream>
10#include <optional>
11#include <sstream>
12#include <unordered_map>
13#include <unordered_set>
14#include <utility>
15
16namespace eve::script {
17namespace {
18
19std::string normalizedPath(std::string value, bool& escaped) {
20 std::replace(value.begin(), value.end(), '\\', '/');
21 std::vector<std::string> parts;
22 size_t begin = 0;
23 while (begin <= value.size()) {
24 const size_t end = value.find('/', begin);
25 const std::string part = value.substr(begin, end - begin);
26 if (part.empty() || part == ".") {
27 } else if (part == "..") {
28 if (parts.empty()) {
29 escaped = true;
30 return {};
31 }
32 parts.pop_back();
33 } else {
34 parts.push_back(part);
35 }
36 if (end == std::string::npos) break;
37 begin = end + 1;
38 }
39 std::ostringstream out;
40 for (size_t i = 0; i < parts.size(); ++i) {
41 if (i) out << '/';
42 out << parts[i];
43 }
44 return out.str();
45}
46
47std::string hashSource(std::string_view source) {
48 uint64_t hash = 1469598103934665603ull;
49 for (const unsigned char c : source) {
50 hash ^= c;
51 hash *= 1099511628211ull;
52 }
53 std::ostringstream out;
54 out << std::hex << hash;
55 return out.str();
56}
57
58class GameFileProvider final : public IScriptModuleProvider {
59public:
60 ScriptModuleStatus resolve(const ScriptModuleRequest& request, std::string& canonicalUri,
61 std::string& error) override {
63 return canonicalUri.rfind("game:/", 0) == 0 ? ScriptModuleStatus::Found : ScriptModuleStatus::NotHandled;
64 }
65
66 ScriptModuleStatus load(std::string_view canonicalUri, ScriptModuleSource& source, std::string& error) override {
67 if (canonicalUri.rfind("game:/", 0) != 0) return ScriptModuleStatus::NotHandled;
68 const std::string path(canonicalUri.substr(6));
69 std::vector<uint8_t> bytes;
70 if (auto* filesystem = eve::cap::query<eve::service::IFileSystem>();
71 filesystem && filesystem->readFile(path, bytes)) {
72 source.utf8Source.assign(reinterpret_cast<const char*>(bytes.data()), bytes.size());
73 } else {
74 std::ifstream input(path, std::ios::binary);
75 if (!input) {
76 error = "script module not found: " + std::string(canonicalUri);
78 }
79 source.utf8Source.assign(std::istreambuf_iterator<char>(input), {});
80 }
81 source.canonicalUri = std::string(canonicalUri);
82 source.debugOrigin = path;
83 source.contentHash = hashSource(source.utf8Source);
85 }
86};
87
88} // namespace
89
92 ProviderId id = 0;
93 int priority = 0;
94 std::shared_ptr<IScriptModuleProvider> provider;
95 };
96 struct Module {
99 HSQOBJECT closure{};
100 HSQOBJECT exports{};
102 std::vector<std::string> dependencies;
103 };
104
105 explicit Impl(SQVM* inVm) : vm(inVm) { sq_setmodulehandlers(vm, &dependencyCallback, &importCallback, this); }
107 sq_setmodulehandlers(vm, nullptr, nullptr, nullptr);
108 for (auto& [_, module] : modules) {
109 if (!sq_isnull(module.closure)) sq_release(vm, &module.closure);
110 if (!sq_isnull(module.exports)) sq_release(vm, &module.exports);
111 }
112 }
113
114 static SQRESULT dependencyCallback(HSQUIRRELVM, const SQChar* importer, const SQChar* specifier,
115 SQUserPointer user) {
116 auto& self = *static_cast<Impl*>(user);
117 try {
118 const std::string dependency = self.ensureCompiled(importer, specifier);
119 auto& edges = self.dependencies[importer];
120 if (std::find(edges.begin(), edges.end(), dependency) == edges.end()) edges.push_back(dependency);
121 return SQ_OK;
122 } catch (const std::exception& error) {
123 self.recordCompilationFailure(error.what());
124 return SQ_ERROR;
125 }
126 }
127
128 static SQRESULT importCallback(HSQUIRRELVM vm, const SQChar* importer, const SQChar* specifier, SQUserPointer user,
129 HSQOBJECT* exports) {
130 auto& self = *static_cast<Impl*>(user);
131 try {
132 const std::string canonical = self.resolve({importer, specifier});
133 const auto found = self.modules.find(canonical);
134 if (found == self.modules.end() || found->second.state != Module::State::Ready)
135 throw std::runtime_error("module was not instantiated before import: " + canonical);
136 *exports = found->second.exports;
137 return SQ_OK;
138 } catch (const std::exception& error) {
139 self.recordCompilationFailure(error.what());
140 return sq_throwerror(vm, error.what());
141 }
142 }
143
147
148 std::string resolve(const ScriptModuleRequest& request) {
149 std::string attempts;
150 for (const ProviderEntry& entry : providers) {
151 std::string canonical;
152 std::string error;
153 const auto status = entry.provider->resolve(request, canonical, error);
154 if (status == ScriptModuleStatus::Found) return canonical;
156 throw std::runtime_error(error.empty() ? "module resolution failed" : error);
157 if (!attempts.empty()) attempts += ", ";
158 attempts += std::to_string(entry.id);
159 }
160 throw std::runtime_error("no script module provider handled '" + request.specifier + "' imported by '" +
161 request.importerUri + "' (providers: " + attempts + ")");
162 }
163
164 ScriptModuleSource load(const std::string& canonical) {
165 for (const ProviderEntry& entry : providers) {
167 std::string error;
168 const auto status = entry.provider->load(canonical, source, error);
169 if (status == ScriptModuleStatus::NotHandled) continue;
171 throw std::runtime_error(error.empty() ? "module load failed" : error);
172 if (source.canonicalUri != canonical)
173 throw std::runtime_error("module provider returned inconsistent identity: " + source.canonicalUri +
174 " != " + canonical);
175 return source;
176 }
177 throw std::runtime_error("no script module provider can load: " + canonical);
178 }
179
180 std::string ensureCompiled(std::string_view importer, std::string_view specifier) {
181 const std::string canonical = resolve({std::string(importer), std::string(specifier)});
182 if (const auto found = modules.find(canonical); found != modules.end()) {
183 if (found->second.state == Module::State::Compiling)
184 throw std::runtime_error("cyclic script import: " + canonical);
185 return canonical;
186 }
187
188 Module& module = modules[canonical];
189 sq_resetobject(&module.closure);
190 sq_resetobject(&module.exports);
191 module.source = load(canonical);
192 module.state = Module::State::Compiling;
193 const SQInteger top = sq_gettop(vm);
194 if (SQ_FAILED(sq_compilebuffer(vm, module.source.utf8Source.c_str(),
195 static_cast<SQInteger>(module.source.utf8Source.size()), canonical.c_str(),
196 SQTrue))) {
197 sq_settop(vm, top);
198 modules.erase(canonical);
200 std::string failure = std::move(*pendingCompilationFailure);
202 throw std::runtime_error(std::move(failure));
203 }
204 throw std::runtime_error("failed to compile script module: " + canonical);
205 }
206 sq_getstackobj(vm, -1, &module.closure);
207 sq_addref(vm, &module.closure);
208 sq_settop(vm, top);
209 module.dependencies = dependencies[canonical];
210 module.state = Module::State::Compiled;
211 return canonical;
212 }
213
214 void instantiate(const std::string& canonical) {
215 auto found = modules.find(canonical);
216 if (found == modules.end()) throw std::runtime_error("unknown script module: " + canonical);
217 instantiateModule(canonical, found->second);
218 }
219
220 void instantiateModule(const std::string& canonical, Module& module) {
221 if (module.state == Module::State::Ready) return;
223 throw std::runtime_error("cyclic script module instantiation: " + canonical);
224 module.state = Module::State::Instantiating;
225 for (const std::string& dependency : module.dependencies) instantiate(dependency);
226
227 const SQInteger top = sq_gettop(vm);
228 sq_newtable(vm); // env
229 sq_pushroottable(vm); // env, root
230 sq_setdelegate(vm, -2); // env
231 sq_pushstring(vm, "__exports", -1); // env, key
232 sq_newtable(vm); // env, key, exports
233 sq_getstackobj(vm, -1, &module.exports);
234 sq_addref(vm, &module.exports);
235 sq_newslot(vm, -3, SQFalse); // env
236 sq_pushobject(vm, module.closure); // env, closure
237 sq_push(vm, -2); // env, closure, env
238 sq_setclosureroot(vm, -2); // env, closure
239 sq_push(vm, -2); // env, closure, env(this)
240 if (SQ_FAILED(sq_call(vm, 1, SQFalse, SQTrue))) {
241 sq_settop(vm, top);
242 module.state = Module::State::Compiled;
243 sq_release(vm, &module.exports);
244 sq_resetobject(&module.exports);
245 throw std::runtime_error("failed to instantiate script module: " + canonical);
246 }
247 sq_settop(vm, top);
248 module.state = Module::State::Ready;
249 }
250
251 void release(Module& module) {
252 if (!sq_isnull(module.closure)) sq_release(vm, &module.closure);
253 if (!sq_isnull(module.exports)) sq_release(vm, &module.exports);
254 sq_resetobject(&module.closure);
255 sq_resetobject(&module.exports);
256 }
257
258 std::vector<std::string> reloadAffected(const std::string& canonical) {
259 if (modules.find(canonical) == modules.end()) return {};
260
261 std::unordered_set<std::string> affected{canonical};
262 bool changed = true;
263 while (changed) {
264 changed = false;
265 for (const auto& [importer, edges] : dependencies) {
266 if (affected.count(importer) || modules.find(importer) == modules.end()) continue;
267 if (std::any_of(edges.begin(), edges.end(), [&](const auto& edge) { return affected.count(edge); })) {
268 affected.insert(importer);
269 changed = true;
270 }
271 }
272 }
273
274 std::vector<std::string> order(affected.begin(), affected.end());
275 std::sort(order.begin(), order.end());
276 const auto previousEdges = dependencies;
277 std::unordered_set<std::string> previousKeys;
278 for (const auto& [uri, _] : modules) previousKeys.insert(uri);
279
280 std::unordered_map<std::string, Module> previous;
281 for (const std::string& uri : order) {
282 auto found = modules.find(uri);
283 if (found == modules.end()) continue;
284 previous.emplace(uri, std::move(found->second));
285 modules.erase(found);
286 dependencies.erase(uri);
287 }
288
289 try {
290 for (const std::string& uri : order) ensureCompiled("game:/__hot_reload__.nut", uri);
291 for (const std::string& uri : order) instantiate(uri);
292 } catch (...) {
293 for (auto it = modules.begin(); it != modules.end();) {
294 if (affected.count(it->first) || !previousKeys.count(it->first)) {
295 release(it->second);
296 it = modules.erase(it);
297 } else {
298 ++it;
299 }
300 }
301 for (auto& [uri, module] : previous) modules.emplace(uri, std::move(module));
302 dependencies = previousEdges;
303 throw;
304 }
305
306 for (auto& [_, module] : previous) release(module);
307 return order;
308 }
309
310 SQVM* vm = nullptr;
312 std::vector<ProviderEntry> providers;
313 std::unordered_map<std::string, Module> modules;
314 std::unordered_map<std::string, std::vector<std::string>> dependencies;
315 std::optional<std::string> pendingCompilationFailure;
316};
317
318ScriptModuleResolver::ScriptModuleResolver(SQVM* vm) : impl_(std::make_unique<Impl>(vm)) {}
320
321ScriptModuleResolver::ProviderId ScriptModuleResolver::registerProvider(std::shared_ptr<IScriptModuleProvider> provider,
322 int priority) {
323 if (!provider) return 0;
324 const ProviderId id = impl_->nextProviderId++;
325 impl_->providers.push_back({id, priority, std::move(provider)});
326 std::stable_sort(impl_->providers.begin(), impl_->providers.end(),
327 [](const auto& a, const auto& b) { return a.priority > b.priority; });
328 return id;
329}
330
332 const auto oldSize = impl_->providers.size();
333 impl_->providers.erase(std::remove_if(impl_->providers.begin(), impl_->providers.end(),
334 [id](const auto& entry) { return entry.id == id; }),
335 impl_->providers.end());
336 return oldSize != impl_->providers.size();
337}
338
339void ScriptModuleResolver::registerDefaultProviders() { registerProvider(std::make_shared<GameFileProvider>()); }
340
341void ScriptModuleResolver::beginCompilation(std::string_view importerUri) {
342 impl_->pendingCompilationFailure.reset();
343 impl_->dependencies.erase(std::string(importerUri));
344}
345
347 std::optional<std::string> failure = std::move(impl_->pendingCompilationFailure);
348 impl_->pendingCompilationFailure.reset();
349 return failure;
350}
351
352void ScriptModuleResolver::prepareDependencies(std::string_view importerUri) {
353 enum class Visit { Visiting, Ready };
354 std::unordered_map<std::string, Visit> visits;
355 std::vector<std::string> stack;
356 const auto verify = [&](const auto& self, const std::string& uri) -> void {
357 if (const auto seen = visits.find(uri); seen != visits.end()) {
358 if (seen->second == Visit::Ready) return;
359 std::string cycle;
360 const auto begin = std::find(stack.begin(), stack.end(), uri);
361 for (auto it = begin; it != stack.end(); ++it) {
362 if (!cycle.empty()) cycle += " -> ";
363 cycle += *it;
364 }
365 if (!cycle.empty()) cycle += " -> ";
366 throw std::runtime_error("cyclic script import: " + cycle + uri);
367 }
368
369 visits.emplace(uri, Visit::Visiting);
370 stack.push_back(uri);
371 const auto edges = impl_->dependencies.find(uri);
372 if (edges != impl_->dependencies.end()) {
373 for (const std::string& dependency : edges->second) {
374 if (impl_->modules.find(dependency) == impl_->modules.end())
375 throw std::runtime_error("script dependency was not compiled: " + dependency);
376 self(self, dependency);
377 }
378 }
379 stack.pop_back();
380 visits[uri] = Visit::Ready;
381 };
382
383 const std::string root(importerUri);
384 const auto edges = impl_->dependencies.find(root);
385 if (edges == impl_->dependencies.end()) return;
386 for (const std::string& dependency : edges->second) verify(verify, dependency);
387}
388
389void ScriptModuleResolver::instantiateDependencies(std::string_view importerUri) {
390 const auto found = impl_->dependencies.find(std::string(importerUri));
391 if (found == impl_->dependencies.end()) return;
392 for (const std::string& dependency : found->second) impl_->instantiate(dependency);
393}
394
395void ScriptModuleResolver::invalidate(std::string_view canonicalUri) {
396 std::unordered_set<std::string> invalid{std::string(canonicalUri)};
397 bool changed = true;
398 while (changed) {
399 changed = false;
400 for (const auto& [importer, deps] : impl_->dependencies) {
401 if (!invalid.count(importer) &&
402 std::any_of(deps.begin(), deps.end(), [&](const auto& dep) { return invalid.count(dep); })) {
403 invalid.insert(importer);
404 changed = true;
405 }
406 }
407 }
408 for (const std::string& uri : invalid) {
409 const auto found = impl_->modules.find(uri);
410 if (found == impl_->modules.end()) continue;
411 if (!sq_isnull(found->second.closure)) sq_release(impl_->vm, &found->second.closure);
412 if (!sq_isnull(found->second.exports)) sq_release(impl_->vm, &found->second.exports);
413 impl_->modules.erase(found);
414 impl_->dependencies.erase(uri);
415 }
416}
417
418void ScriptModuleResolver::reload(std::string_view canonicalUri) {
419 if (impl_->reloadAffected(std::string(canonicalUri)).empty())
420 throw std::runtime_error("cannot reload unknown script module: " + std::string(canonicalUri));
421}
422
423std::vector<std::string> ScriptModuleResolver::reloadAffected(std::string_view canonicalUri) {
424 return impl_->reloadAffected(std::string(canonicalUri));
425}
426
427std::vector<std::string> ScriptModuleResolver::dependencies(std::string_view importerUri) const {
428 const auto found = impl_->dependencies.find(std::string(importerUri));
429 return found == impl_->dependencies.end() ? std::vector<std::string>{} : found->second;
430}
431
432std::vector<std::string> ScriptModuleResolver::reverseDependencies(std::string_view canonicalUri) const {
433 std::vector<std::string> result;
434 for (const auto& [importer, dependencies] : impl_->dependencies) {
435 if (std::find(dependencies.begin(), dependencies.end(), canonicalUri) != dependencies.end())
436 result.push_back(importer);
437 }
438 std::sort(result.begin(), result.end());
439 return result;
440}
441
443 std::string scheme = "game:";
444 std::string path;
445 const size_t schemeEnd = request.specifier.find(":/");
446 if (schemeEnd != std::string::npos) {
447 scheme = request.specifier.substr(0, schemeEnd + 1);
448 path = request.specifier.substr(schemeEnd + 2);
449 } else if (request.specifier.rfind("./", 0) == 0 || request.specifier.rfind("../", 0) == 0) {
450 std::string importer = request.importerUri;
451 const size_t importerScheme = importer.find(":/");
452 if (importerScheme != std::string::npos) {
453 scheme = importer.substr(0, importerScheme + 1);
454 importer = importer.substr(importerScheme + 2);
455 }
456 const size_t slash = importer.find_last_of("/\\");
457 path = (slash == std::string::npos ? std::string() : importer.substr(0, slash + 1)) + request.specifier;
458 } else {
459 path = request.specifier;
460 }
461 bool escaped = false;
462 path = normalizedPath(path, escaped);
463 if (escaped || path.empty()) {
464 error = escaped ? "script import escapes its module root: " + request.specifier : "empty script module path";
465 return false;
466 }
467 output = scheme + "/" + path;
468 return true;
469}
470
471} // namespace eve::script
double value
SQInteger top
int root
Definition AnimSmr.cpp:119
std::vector< eve::artifact::PartView > parts
std::string output
int priority
struct SQVM * HSQUIRRELVM
HSQUIRRELVM vm
Definition ECS.cpp:20
std::array< std::uint8_t, 32 > hash
Definition Evpack.cpp:172
EvpackChunkInput input
Definition Evpack.cpp:170
std::string message
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
std::int32_t c
std::uint64_t bytes
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< std::int32_t > order
graphics::Canvas * previous
std::string error
Definition Package.cpp:60
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
std::string uri
float begin
std::function< eve::Result< void >()> release
Definition Procgen.cpp:86
const RoadEdge * edge
bool found
const UnitySourceAsset & source
std::vector< int > edges
void instantiateDependencies(std::string_view importerUri)
Instantiates the importer's dependencies in topological order.
ProviderId registerProvider(std::shared_ptr< IScriptModuleProvider > provider, int priority=0)
Registers a provider. Higher priorities are tried first.
std::vector< std::string > dependencies(std::string_view importerUri) const
Returns canonical direct dependencies for an importer.
void beginCompilation(std::string_view importerUri)
Clears prior diagnostics and dependency edges before compiling an importer.
std::optional< std::string > takeCompilationFailure()
Consumes the native failure captured while resolving the current compilation.
std::vector< std::string > reverseDependencies(std::string_view canonicalUri) const
Returns canonical importers that directly depend on a module.
std::vector< std::string > reloadAffected(std::string_view canonicalUri)
Transactionally reloads a module and every cached module that imports it.
void invalidate(std::string_view canonicalUri)
Invalidates a module and all reverse dependencies.
void reload(std::string_view canonicalUri)
Transactionally reloads one module, retaining the prior generation on failure.
~ScriptModuleResolver()
Script module resolver.
void registerDefaultProviders()
Installs the default game:/ provider backed by IFileSystem and disk fallback.
ScriptModuleResolver(SQVM *vm)
Script module resolver.
static bool canonicalize(const ScriptModuleRequest &request, std::string &output, std::string &error)
Normalizes a logical module URI and rejects root traversal.
bool unregisterProvider(ProviderId id)
Removes a provider; existing compiled module records remain valid.
void prepareDependencies(std::string_view importerUri)
Compiles all imports discovered while compiling an importer.
ScriptModuleStatus
Result of asking a provider to resolve or load a script module.
Logical module request emitted by an EveScript import declaration.
std::shared_ptr< IScriptModuleProvider > provider
void instantiate(const std::string &canonical)
static SQRESULT dependencyCallback(HSQUIRRELVM, const SQChar *importer, const SQChar *specifier, SQUserPointer user)
std::string ensureCompiled(std::string_view importer, std::string_view specifier)
std::unordered_map< std::string, Module > modules
std::optional< std::string > pendingCompilationFailure
std::unordered_map< std::string, std::vector< std::string > > dependencies
static SQRESULT importCallback(HSQUIRRELVM vm, const SQChar *importer, const SQChar *specifier, SQUserPointer user, HSQOBJECT *exports)
ScriptModuleSource load(const std::string &canonical)
std::vector< std::string > reloadAffected(const std::string &canonical)
void instantiateModule(const std::string &canonical, Module &module)
std::string resolve(const ScriptModuleRequest &request)
std::vector< ProviderEntry > providers
void recordCompilationFailure(std::string message)
UTF-8 source and stable identity returned by a module provider.