Index: source/scriptinterface/ScriptExtraHeaders.h =================================================================== --- source/scriptinterface/ScriptExtraHeaders.h +++ source/scriptinterface/ScriptExtraHeaders.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -59,6 +59,7 @@ #include "js/SourceText.h" #include "js/Proxy.h" #include "js/Warnings.h" +#include #undef signbit Index: source/scriptinterface/ScriptInterface.h =================================================================== --- source/scriptinterface/ScriptInterface.h +++ source/scriptinterface/ScriptInterface.h @@ -55,6 +55,8 @@ // use their own threads and also their own contexts. extern thread_local std::shared_ptr g_ScriptContext; +class ScriptModuleLoader; + namespace boost { namespace random { class rand48; } } class Path; @@ -140,6 +142,8 @@ JSContext* GetGeneralJSContext() const; std::shared_ptr GetContext() const; + const ScriptModuleLoader& GetScriptModuleLoader() const; + /** * Load global scripts that most script interfaces need, * located in the /globalscripts directory. VFS must be initialized. @@ -203,6 +207,11 @@ */ bool LoadGlobalScriptFile(const VfsPath& path) const; + /** + * Load an ES module. VFS must be initialized. + */ + bool LoadESModule(const VfsPath& path) const; + /** * Evaluate some JS code in the global scope. * @return true on successful compilation and execution; false otherwise Index: source/scriptinterface/ScriptInterface.cpp =================================================================== --- source/scriptinterface/ScriptInterface.cpp +++ source/scriptinterface/ScriptInterface.cpp @@ -23,6 +23,7 @@ #include "ScriptInterface.h" #include "ScriptStats.h" #include "StructuredClone.h" +#include "ScriptModuleLoader.h" #include "lib/debug.h" #include "lib/utf8.h" @@ -66,6 +67,7 @@ public: boost::rand48* m_rng; JS::PersistentRootedObject m_nativeScope; // native function scope object + ScriptModuleLoader* m_moduleLoader; }; /** @@ -300,7 +302,7 @@ } ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const std::shared_ptr& context, JS::Compartment* compartment) : - m_context(context), m_cx(context->GetGeneralJSContext()), m_glob(context->GetGeneralJSContext()), m_nativeScope(context->GetGeneralJSContext()) + m_context(context), m_cx(context->GetGeneralJSContext()), m_glob(context->GetGeneralJSContext()), m_nativeScope(context->GetGeneralJSContext()), m_moduleLoader(new ScriptModuleLoader()) { JS::RealmCreationOptions creationOpt; // Keep JIT code during non-shrinking GCs. This brings a quite big performance improvement. @@ -343,6 +345,7 @@ ScriptInterface_impl::~ScriptInterface_impl() { + delete m_moduleLoader; m_context->UnRegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } @@ -463,6 +466,11 @@ return m->m_context; } +const ScriptModuleLoader& ScriptInterface::GetScriptModuleLoader() const +{ + return *(m->m_moduleLoader); +} + void ScriptInterface::CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const { ScriptRequest rq(this); @@ -684,6 +692,12 @@ return LoadGlobalScript(path, code); } +bool ScriptInterface::LoadESModule(const VfsPath& path) const +{ + ScriptRequest rq(this); + return m->m_moduleLoader->Evaluate(rq.cx, path); +} + bool ScriptInterface::Eval(const char* code) const { ScriptRequest rq(this); Index: source/scriptinterface/ScriptModuleLoader.h =================================================================== --- /dev/null +++ source/scriptinterface/ScriptModuleLoader.h @@ -0,0 +1,52 @@ +/* Copyright (C) 2022 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_SCRIPTMODULELOADER +#define INCLUDED_SCRIPTMODULELOADER + +#include "lib/file/vfs/vfs_path.h" +#include "ps/Errors.h" +#include "scriptinterface/ScriptConversions.h" +#include "scriptinterface/ScriptExceptions.h" +#include "scriptinterface/ScriptRequest.h" +#include "scriptinterface/ScriptTypes.h" + +#include +#include + +class ScriptModuleLoader +{ +public: + ScriptModuleLoader() = default; + ~ScriptModuleLoader() = default; + + bool Evaluate(JSContext* cx, const VfsPath& modulePath); + +private: + + static JSObject* ModuleResolutionHook(JSContext* cx, JS::HandleValue referencingModule, JS::HandleString path); + + JSObject* CompileModule(JSContext* cx, const VfsPath& filePath) const; + + VfsPath GetScriptPath(JSContext* cx, JS::HandleValue moduleHandle) const; + + VfsPath NormalizeScriptPath(const VfsPath& path) const; + + mutable std::map m_ResolvedModules; +}; + +#endif // INCLUDED_SCRIPTMODULELOADER Index: source/scriptinterface/ScriptModuleLoader.cpp =================================================================== --- /dev/null +++ source/scriptinterface/ScriptModuleLoader.cpp @@ -0,0 +1,180 @@ +/* Copyright (C) 2022 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "ScriptModuleLoader.h" + +#include "ScriptContext.h" +#include "ScriptConversions.h" +#include "ScriptExtraHeaders.h" +#include "ScriptInterface.h" + +#include "lib/debug.h" +#include "lib/utf8.h" +#include "ps/CLogger.h" +#include "ps/Filesystem.h" +#include "ps/CStr.h" + +#include +#include +#include + +VfsPath ScriptModuleLoader::NormalizeScriptPath(const VfsPath& path) const +{ + VfsPath ret; + + std::vector dirs; + std::wstringstream wss(path.string()); + std::wstring dir; + + while (std::getline(wss, dir, L'/')) + { + dirs.push_back(dir); + } + + for (const std::wstring& dir : dirs) + { + if (dir == L".") { + continue; + } + + if (dir == L"..") { + ret = ret.Parent(); + continue; + } + + ret = ret / VfsPath(dir); + } + + return ret; +} + +JSObject* ScriptModuleLoader::CompileModule(JSContext* cx, const VfsPath& filePath) const +{ + ScriptRequest rq(cx); + if (!VfsFileExists(filePath)) + { + LOGERROR("File '%s' does not exist", filePath.string8()); + return nullptr; + } + + if (filePath.Extension() != L".mjs" && false) + { + LOGERROR("File '%s' is not an ES module", filePath.string8()); + return nullptr; + } + + CVFSFile file; + + PSRETURN ret = file.Load(g_VFS, filePath); + if (ret != PSRETURN_OK) + { + LOGERROR("Failed to load file '%s': %s", filePath.string8(), GetErrorString(ret)); + return nullptr; + } + + CStr code = file.DecodeUTF8(); + + JS::CompileOptions options(cx); + options.setFileAndLine(filePath.string8().c_str(), 1); + + JS::SourceText src; + ENSURE(src.init(cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); + + JS::RootedObject mod(cx); + mod = JS::CompileModule(cx, options, src); + + JS::RootedObject modInfo(cx, JS_NewPlainObject(cx)); + JS::RootedValue modPath(cx); + Script::ToJSVal(rq, &modPath, filePath.Parent()); + JS_DefineProperty(cx, modInfo, "path", modPath, JSPROP_ENUMERATE); + JS::SetModulePrivate(mod, JS::ObjectValue(*modInfo)); + + return mod; +} + +VfsPath ScriptModuleLoader::GetScriptPath(JSContext* cx, JS::HandleValue moduleHandle) const +{ + std::wstring ret; + + if (!moduleHandle.isUndefined()) + { + ScriptRequest rq(cx); + Script::FromJSProperty(rq, moduleHandle, "path", ret); + } + + return VfsPath(ret); +} + +JSObject* ScriptModuleLoader::ModuleResolutionHook(JSContext* cx, JS::HandleValue referencingModule, JS::HandleString path) +{ + ScriptRequest rq(cx); + const ScriptModuleLoader& instance = rq.GetScriptInterface().GetScriptModuleLoader(); + + size_t bufferLen; + JS::AutoCheckCannotGC nogc; + const unsigned char* strBuffer = JS_GetLatin1StringCharsAndLength(cx, nogc, path, &bufferLen); + std::wstring pathString; + pathString.assign(strBuffer, strBuffer + bufferLen); + + VfsPath filename = VfsPath(pathString); + VfsPath refFilename = instance.GetScriptPath(cx, referencingModule); + + VfsPath actualPath = instance.NormalizeScriptPath(refFilename / filename); + + std::map::iterator it = instance.m_ResolvedModules.find(actualPath); + if (it != instance.m_ResolvedModules.end()) + { + return it->second; + } + + JS::RootedObject mod(cx, instance.CompileModule(cx, actualPath)); + + if (!mod) + { + ScriptException::CatchPending(rq); + return nullptr; + } + + instance.m_ResolvedModules.emplace(actualPath, JS::PersistentRootedObject(cx, mod)); + + return mod; +} + +bool ScriptModuleLoader::Evaluate(JSContext* cx, const VfsPath& modulePath) +{ + ScriptRequest rq(cx); + + JS::RootedObject global(cx, rq.glob); + if (!global) + { + return false; + } + + JS::SetModuleResolveHook(JS_GetRuntime(cx), ModuleResolutionHook); + + JS::RootedObject topModule(cx, CompileModule(cx, modulePath)); + + if (!topModule || !JS::ModuleInstantiate(cx, topModule) || !JS::ModuleEvaluate(cx, topModule)) + { + ScriptException::CatchPending(rq); + return false; + } + + return true; +} Index: source/scriptinterface/ScriptRequest.h =================================================================== --- source/scriptinterface/ScriptRequest.h +++ source/scriptinterface/ScriptRequest.h @@ -56,6 +56,7 @@ * * Be particularly careful when manipulating several script interfaces. */ + class ScriptRequest { ScriptRequest() = delete;