diff --git a/skyrim-platform/src/platform_se/CMakeLists.txt b/skyrim-platform/src/platform_se/CMakeLists.txt index 0b35a78f5..96b974906 100644 --- a/skyrim-platform/src/platform_se/CMakeLists.txt +++ b/skyrim-platform/src/platform_se/CMakeLists.txt @@ -95,6 +95,9 @@ if(NOT "${SKIP_SKYRIM_PLATFORM_BUILDING}") target_include_directories(skyrim_platform PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/include) target_link_libraries(skyrim_platform PRIVATE papyrus-vm-lib savefile skyrim_plugin_resources ui cef platform_lib viet serialization) target_link_libraries(skyrim_platform PRIVATE node-embedder-api) + + find_package(qjs CONFIG REQUIRED) + target_link_libraries(skyrim_platform PRIVATE qjs) find_path(FRIDA_GUM_INCLUDE_DIR NAMES frida-gum.h) find_library(FRIDA_GUM_LIBRARY NAMES frida-gum.lib) diff --git a/skyrim-platform/src/platform_se/skyrim_platform/ConsoleApi.cpp b/skyrim-platform/src/platform_se/skyrim_platform/ConsoleApi.cpp index aadc21718..e33b7594c 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/ConsoleApi.cpp +++ b/skyrim-platform/src/platform_se/skyrim_platform/ConsoleApi.cpp @@ -340,7 +340,7 @@ bool ConsoleComand_Execute(const RE::SCRIPT_PARAMETER* paramInfo, } }; - SkyrimPlatform::GetSingleton()->PushAndWait(func); + SkyrimPlatform::GetSingleton()->RunTask(func); if (iterator) iterator->second.execute(paramInfo, scriptData, thisObj, containingObj, scriptObj, locals, result, opcodeOffsetPtr); diff --git a/skyrim-platform/src/platform_se/skyrim_platform/EventsApi.cpp b/skyrim-platform/src/platform_se/skyrim_platform/EventsApi.cpp index f68d86ede..d1f0d5851 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/EventsApi.cpp +++ b/skyrim-platform/src/platform_se/skyrim_platform/EventsApi.cpp @@ -1,9 +1,9 @@ #include "EventsApi.h" #include "EventManager.h" #include "ExceptionPrinter.h" -#include "Handler.h" #include "Hook.h" #include "HookPattern.h" +#include "HooksStorage.h" #include "IPC.h" #include "InvalidArgumentException.h" #include "JsUtils.h" @@ -112,10 +112,14 @@ namespace { Napi::Value CreateHookApi(Napi::Env env, std::shared_ptr hookInfo) { auto hook = Napi::Object::New(env); + + // hooks.sendAnimationEvent.add(sourceCode, [minSelfId], [maxSelfId], + // [pattern]) + // sourceCode: string containing JS with enter(ctx) and/or leave(ctx) hook.Set( "add", Napi::Function::New(env, [hookInfo](const Napi::CallbackInfo& info) { - auto handlerObj = NapiHelper::ExtractObject(info[0], "handlerObj"); + auto source = NapiHelper::ExtractString(info[0], "source"); std::optional minSelfId; if (info[1].IsNumber()) { @@ -133,9 +137,8 @@ Napi::Value CreateHookApi(Napi::Env env, std::shared_ptr hookInfo) pattern = HookPattern::Parse(s); } - auto handler = - std::make_shared(handlerObj, minSelfId, maxSelfId, pattern); - uint32_t id = hookInfo->AddHandler(handler); + uint32_t id = + hookInfo->AddScript(source, minSelfId, maxSelfId, std::move(pattern)); return Napi::Number::New(info.Env(), id); })); @@ -144,7 +147,7 @@ Napi::Value CreateHookApi(Napi::Env env, std::shared_ptr hookInfo) "remove", Napi::Function::New(env, [hookInfo](const Napi::CallbackInfo& info) { uint32_t toRemove = NapiHelper::ExtractUInt32(info[0], "toRemove"); - hookInfo->RemoveHandler(toRemove); + hookInfo->RemoveScript(toRemove); return info.Env().Undefined(); })); return hook; @@ -160,6 +163,74 @@ Napi::Value EventsApi::GetHooks(Napi::Env env) return res; } +Napi::Value EventsApi::GetHooksStorage(Napi::Env env) +{ + auto obj = Napi::Object::New(env); + + obj.Set("get", Napi::Function::New(env, [](const Napi::CallbackInfo& info) { + auto key = NapiHelper::ExtractString(info[0], "key"); + auto val = HooksStorage::GetSingleton().Get(key); + + Napi::Env env = info.Env(); + if (std::holds_alternative(val)) { + return env.Undefined(); + } + if (std::holds_alternative(val)) { + return static_cast( + Napi::Boolean::New(env, std::get(val))); + } + if (std::holds_alternative(val)) { + return static_cast( + Napi::Number::New(env, std::get(val))); + } + if (std::holds_alternative(val)) { + return static_cast( + Napi::String::New(env, std::get(val))); + } + return env.Undefined(); + })); + + obj.Set("set", Napi::Function::New(env, [](const Napi::CallbackInfo& info) { + auto key = NapiHelper::ExtractString(info[0], "key"); + Napi::Value val = info[1]; + + if (val.IsBoolean()) { + HooksStorage::GetSingleton().Set(key, val.As().Value()); + } else if (val.IsNumber()) { + HooksStorage::GetSingleton().Set(key, + val.As().DoubleValue()); + } else if (val.IsString()) { + HooksStorage::GetSingleton().Set( + key, val.As().Utf8Value()); + } else { + HooksStorage::GetSingleton().Set(key, std::monostate{}); + } + + return info.Env().Undefined(); + })); + + obj.Set("has", Napi::Function::New(env, [](const Napi::CallbackInfo& info) { + auto key = NapiHelper::ExtractString(info[0], "key"); + return Napi::Boolean::New(info.Env(), + HooksStorage::GetSingleton().Has(key)); + })); + + obj.Set( + "erase", Napi::Function::New(env, [](const Napi::CallbackInfo& info) { + auto key = NapiHelper::ExtractString(info[0], "key"); + HooksStorage::GetSingleton().Erase(key); + return info.Env().Undefined(); + })); + + obj.Set( + "clear", Napi::Function::New(env, [](const Napi::CallbackInfo& info) { + HooksStorage::GetSingleton().Clear(); + return info.Env().Undefined(); + })); + + return obj; +} + namespace { Napi::Value Subscribe(const Napi::CallbackInfo& info, bool runOnce = false) { diff --git a/skyrim-platform/src/platform_se/skyrim_platform/EventsApi.h b/skyrim-platform/src/platform_se/skyrim_platform/EventsApi.h index b20c7a367..a5a102bf4 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/EventsApi.h +++ b/skyrim-platform/src/platform_se/skyrim_platform/EventsApi.h @@ -21,6 +21,7 @@ void SendPapyrusEventEnter(uint32_t selfId, void SendPapyrusEventLeave() noexcept; Napi::Value GetHooks(Napi::Env env); +Napi::Value GetHooksStorage(Napi::Env env); inline void Register(Napi::Env env, Napi::Object& exports) { @@ -29,6 +30,7 @@ inline void Register(Napi::Env env, Napi::Object& exports) exports.Set("once", Napi::Function::New(env, NapiHelper::WrapCppExceptions(Once))); exports.Set("hooks", GetHooks(env)); + exports.Set("hooksStorage", GetHooksStorage(env)); exports.Set( "sendIpcMessage", Napi::Function::New(env, NapiHelper::WrapCppExceptions(SendIpcMessage))); diff --git a/skyrim-platform/src/platform_se/skyrim_platform/Handler.h b/skyrim-platform/src/platform_se/skyrim_platform/Handler.h index d86df0407..a8f5520fe 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/Handler.h +++ b/skyrim-platform/src/platform_se/skyrim_platform/Handler.h @@ -1,13 +1,6 @@ #pragma once #include "HookPattern.h" -// HandlerInfoPerThread structure is unique for each thread -struct HandlerInfoPerThread -{ - std::shared_ptr> storage, context; - bool matchesCondition = false; -}; - class Handler { public: @@ -19,8 +12,6 @@ class Handler bool Matches(uint32_t selfId, const std::string& eventName); - std::unordered_map perThread; - // Shared between threads const Napi::Reference enter, leave; const std::optional pattern; diff --git a/skyrim-platform/src/platform_se/skyrim_platform/Hook.cpp b/skyrim-platform/src/platform_se/skyrim_platform/Hook.cpp index 6fa4378fd..99e372132 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/Hook.cpp +++ b/skyrim-platform/src/platform_se/skyrim_platform/Hook.cpp @@ -1,30 +1,85 @@ #include "Hook.h" -#include "Handler.h" -#include "SkyrimPlatform.h" +#include "ScopedTask.h" +#include +#include + +std::mutex Hook::g_invocationsMutex; +std::map> Hook::g_invocations; + +bool HookScriptEntry::Matches(uint32_t selfId, + const std::string& eventName) const +{ + if (minSelfId.has_value() && selfId < minSelfId.value()) { + return false; + } + if (maxSelfId.has_value() && selfId > maxSelfId.value()) { + return false; + } + if (pattern.has_value()) { + switch (pattern->type) { + case HookPatternType::Exact: + return eventName == pattern->str; + case HookPatternType::StartsWith: + return eventName.size() >= pattern->str.size() && + !memcmp(eventName.data(), pattern->str.data(), pattern->str.size()); + case HookPatternType::EndsWith: + return eventName.size() >= pattern->str.size() && + !memcmp(eventName.data() + (eventName.size() - pattern->str.size()), + pattern->str.data(), pattern->str.size()); + } + } + return true; +} Hook::Hook(std::string hookName_, std::string eventNameVariableName_, std::optional succeededVariableName_) - : hookName(hookName_) - , eventNameVariableName(eventNameVariableName_) - , succeededVariableName(succeededVariableName_) + : hookName(std::move(hookName_)) + , eventNameVariableName(std::move(eventNameVariableName_)) + , succeededVariableName(std::move(succeededVariableName_)) { } -uint32_t Hook::AddHandler(const std::shared_ptr& handler) +uint32_t Hook::AddScript(const std::string& source, + std::optional minSelfId, + std::optional maxSelfId, + std::optional pattern) { if (addRemoveBlocker) { throw std::runtime_error("Trying to add hook inside hook context"); } - handlers.emplace(hCounter, handler); - return hCounter++; + + std::string error; + uint32_t qjsId = qjsEngine->Compile(source, hookName + "_hook", error); + + if (qjsId == 0) { + throw std::runtime_error("Hook script compilation failed: " + error); + } + + uint32_t handleId = nextHandlerId++; + + HookScriptEntry entry; + entry.qjsScriptId = qjsId; + entry.pattern = std::move(pattern); + entry.minSelfId = minSelfId; + entry.maxSelfId = maxSelfId; + scripts[handleId] = std::move(entry); + + return handleId; } -void Hook::RemoveHandler(const uint32_t& id) +void Hook::RemoveScript(uint32_t id) { if (addRemoveBlocker) { throw std::runtime_error("Trying to remove hook inside hook context"); } - handlers.erase(id); + + auto it = scripts.find(id); + if (it == scripts.end()) { + return; + } + + qjsEngine->RemoveScript(it->second.qjsScriptId); + scripts.erase(it); } std::string Hook::GetName() const @@ -35,155 +90,54 @@ std::string Hook::GetName() const void Hook::Enter(uint32_t selfId, std::string& eventName) { addRemoveBlocker++; - DWORD owningThread = GetCurrentThreadId(); - - if (hookName == "sendPapyrusEvent") { - // If there are no handlers, do not do g_taskQueue - bool anyMatch = false; - for (auto& hp : handlers) { - auto* h = hp.second.get(); - if (h->Matches(selfId, eventName)) { - anyMatch = true; - break; - } - } - if (!anyMatch) { - return; - } + Viet::ScopedTask t([](Hook& hook) { hook.addRemoveBlocker--; }, *this); - return SkyrimPlatform::GetSingleton()->AddUpdateTask( - [this, owningThread, selfId, eventName](Napi::Env env) { - std::string s = eventName; - HandleEnter(owningThread, selfId, s, env); - }); - } + InvocationInfo invInfo; - auto f = [&](Napi::Env env) { - try { - if (inProgressThreads.count(owningThread)) - throw std::runtime_error("'" + hookName + "' is already processing"); - inProgressThreads.insert(owningThread); - HandleEnter(owningThread, selfId, eventName, env); - } catch (std::exception& e) { - auto err = std::string(e.what()) + " (while performing enter on '" + - hookName + "')"; - SkyrimPlatform::GetSingleton()->AddUpdateTask( - [err](Napi::Env) { throw std::runtime_error(err); }); - } - }; - SkyrimPlatform::GetSingleton()->PushAndWait(f); - addRemoveBlocker--; -} - -void Hook::Leave(bool succeeded) -{ - addRemoveBlocker++; - DWORD owningThread = GetCurrentThreadId(); - - if (hookName == "sendPapyrusEvent") { - return; - } - - auto f = [&](Napi::Env env) { - try { - if (!inProgressThreads.count(owningThread)) - throw std::runtime_error("'" + hookName + "' is not processing"); - inProgressThreads.erase(owningThread); - HandleLeave(owningThread, succeeded, env); - - } catch (std::exception& e) { - std::string what = e.what(); - SkyrimPlatform::GetSingleton()->AddUpdateTask([what](Napi::Env) { - throw std::runtime_error(what + " (in SendAnimationEventLeave)"); - }); + // Collect matching scripts and run enter + for (auto& [handleId, entry] : scripts) { + if (!entry.Matches(selfId, eventName)) { + continue; } - }; - SkyrimPlatform::GetSingleton()->PushAndWait(f); - addRemoveBlocker--; -} + invInfo.matchedScriptIds.push_back(entry.qjsScriptId); -void Hook::HandleEnter(DWORD owningThread, uint32_t selfId, - std::string& eventName, const Napi::Env& env) -{ - for (auto& hp : handlers) { - Handler* h = hp.second.get(); - auto& perThread = h->perThread[owningThread]; - - perThread.matchesCondition = h->Matches(selfId, eventName); - if (!perThread.matchesCondition) { + auto result = qjsEngine->RunEnter(entry.qjsScriptId, selfId, eventName); + if (!result.ok) { + spdlog::error("Hook '{}' enter error: {}", hookName, result.error); continue; } - - PrepareContext(perThread, env); - ClearContextStorage(perThread, env); - - perThread.context->Value().As().Set( - "selfId", Napi::Number::New(env, static_cast(selfId))); - perThread.context->Value().As().Set( - eventNameVariableName, Napi::String::New(env, eventName)); - h->enter.Value().As().Call(env.Undefined(), - { perThread.context->Value() }); - - // Retrieve the updated eventName from the context - Napi::Value updatedEventName = - perThread.context->Value().As().Get(eventNameVariableName); - eventName = updatedEventName.As().Utf8Value(); + eventName = result.eventName; } -} -void Hook::PrepareContext(HandlerInfoPerThread& h, const Napi::Env& env) -{ - if (!h.context) { - auto object = Napi::Object::New(env); - h.context.reset(new Napi::Reference( - Napi::Persistent(object))); - } - - Napi::Value standardMap = env.Global().Get("Map"); - - if (!h.storage) { - Napi::Object mapInstance = standardMap.As().New({}); - h.storage.reset( - new Napi::Reference(Napi::Persistent(mapInstance))); - h.context->Value().As().Set("storage", h.storage->Value()); + // Push invocation info for Leave to use + const DWORD threadId = GetCurrentThreadId(); + { + std::lock_guard l(g_invocationsMutex); + g_invocations[threadId].push_back(std::move(invInfo)); } } -void Hook::ClearContextStorage(HandlerInfoPerThread& h, Napi::Env env) -{ - if (!h.storage) { - return; - } - - Napi::Object global = env.Global(); - Napi::Object standardMap = global.Get("Map").As(); - Napi::Function clear = standardMap.Get("prototype") - .As() - .Get("clear") - .As(); - - clear.Call(h.storage->Value(), {}); -} - -void Hook::HandleLeave(DWORD owningThread, bool succeeded, Napi::Env env) +void Hook::Leave(bool succeeded) { - for (auto& hp : handlers) { - Handler* h = hp.second.get(); - auto& perThread = h->perThread.at(owningThread); - - if (!perThread.matchesCondition) { - continue; + addRemoveBlocker++; + Viet::ScopedTask t([](Hook& hook) { hook.addRemoveBlocker--; }, *this); + + const DWORD threadId = GetCurrentThreadId(); + InvocationInfo invInfo; + { + std::lock_guard l(g_invocationsMutex); + auto& stack = g_invocations[threadId]; + if (stack.empty()) { + return; } + invInfo = std::move(stack.back()); + stack.pop_back(); + } - PrepareContext(perThread, env); - - if (succeededVariableName.has_value()) { - perThread.context->Value().As().Set( - succeededVariableName.value(), Napi::Boolean::New(env, succeeded)); + for (uint32_t scriptId : invInfo.matchedScriptIds) { + auto result = qjsEngine->RunLeave(scriptId, succeeded); + if (!result.ok) { + spdlog::error("Hook '{}' leave error: {}", hookName, result.error); } - - h->leave.Value().As().Call(env.Undefined(), - { perThread.context->Value() }); - h->perThread.erase(owningThread); } } diff --git a/skyrim-platform/src/platform_se/skyrim_platform/Hook.h b/skyrim-platform/src/platform_se/skyrim_platform/Hook.h index 74ce1ce77..817c99006 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/Hook.h +++ b/skyrim-platform/src/platform_se/skyrim_platform/Hook.h @@ -1,7 +1,24 @@ #pragma once +#include "HookPattern.h" +#include "QuickJSHookEngine.h" +#include +#include +#include +#include +#include +#include +#include -class Handler; -struct HandlerInfoPerThread; +// Represents a single registered hook script with optional filtering +struct HookScriptEntry +{ + uint32_t qjsScriptId = 0; // ID in QuickJSHookEngine + std::optional pattern; + std::optional minSelfId; + std::optional maxSelfId; + + bool Matches(uint32_t selfId, const std::string& eventName) const; +}; class Hook { @@ -9,36 +26,41 @@ class Hook Hook(std::string hookName_, std::string eventNameVariableName_, std::optional succeededVariableName_); - // javascript thread only - uint32_t AddHandler(const std::shared_ptr& handler); - void RemoveHandler(const uint32_t& id); + // Add a hook by providing JavaScript source code. + // The source must define enter(ctx) and/or leave(ctx) functions. + // Returns a handle ID that can be used to remove the hook. + uint32_t AddScript(const std::string& source, + std::optional minSelfId, + std::optional maxSelfId, + std::optional pattern); - // Thread-safe, but it isn't too useful actually - std::string GetName() const; + void RemoveScript(uint32_t id); - // Hooks are set on game functions that are being called from multiple - // threads. So Enter/Leave methods are thread-safe, but all private methods - // are for Chakra thread only + std::string GetName() const; + // Thread-safe entry/exit points called from game hooks void Enter(uint32_t selfId, std::string& eventName); - void Leave(bool succeeded); private: - void HandleEnter(DWORD owningThread, uint32_t selfId, std::string& eventName, - const Napi::Env& env); - - void PrepareContext(HandlerInfoPerThread& h, const Napi::Env& env); - - void ClearContextStorage(HandlerInfoPerThread& h, Napi::Env env); - - void HandleLeave(DWORD owningThread, bool succeeded, Napi::Env env); - const std::string hookName; const std::string eventNameVariableName; const std::optional succeededVariableName; - std::set inProgressThreads; - std::map> handlers; - uint32_t hCounter = 0; + + // QuickJS engine owns all compiled hook scripts for this hook + std::unique_ptr qjsEngine; + + std::map scripts; + uint32_t nextHandlerId = 0; + + // Prevent add/remove while executing hooks std::atomic addRemoveBlocker = 0; + + // Per-thread invocation tracking for nested Enter/Leave pairs + static std::mutex g_invocationsMutex; + struct InvocationInfo + { + std::vector matchedScriptIds; + }; + static std::map> g_invocations; }; diff --git a/skyrim-platform/src/platform_se/skyrim_platform/HooksStorage.cpp b/skyrim-platform/src/platform_se/skyrim_platform/HooksStorage.cpp new file mode 100644 index 000000000..f55309bd3 --- /dev/null +++ b/skyrim-platform/src/platform_se/skyrim_platform/HooksStorage.cpp @@ -0,0 +1,47 @@ +#include "HooksStorage.h" + +HooksStorage& HooksStorage::GetSingleton() +{ + static HooksStorage instance; + return instance; +} + +void HooksStorage::Set(const std::string& key, Value value) +{ + std::lock_guard lock(mtx); + data[key] = std::move(value); +} + +HooksStorage::Value HooksStorage::Get(const std::string& key) const +{ + std::lock_guard lock(mtx); + auto it = data.find(key); + if (it == data.end()) { + return std::monostate{}; + } + return it->second; +} + +bool HooksStorage::Has(const std::string& key) const +{ + std::lock_guard lock(mtx); + return data.count(key) > 0; +} + +void HooksStorage::Erase(const std::string& key) +{ + std::lock_guard lock(mtx); + data.erase(key); +} + +void HooksStorage::Clear() +{ + std::lock_guard lock(mtx); + data.clear(); +} + +std::map HooksStorage::Snapshot() const +{ + std::lock_guard lock(mtx); + return data; +} diff --git a/skyrim-platform/src/platform_se/skyrim_platform/HooksStorage.h b/skyrim-platform/src/platform_se/skyrim_platform/HooksStorage.h new file mode 100644 index 000000000..be347f067 --- /dev/null +++ b/skyrim-platform/src/platform_se/skyrim_platform/HooksStorage.h @@ -0,0 +1,30 @@ +#pragma once +#include +#include +#include +#include + +// Thread-safe key-value storage shared between Node.js and QuickJS contexts. +// Values are stored as C++ variants; both runtimes get/set through the same +// mutex-protected map. +class HooksStorage +{ +public: + using Value = std::variant; + + static HooksStorage& GetSingleton(); + + void Set(const std::string& key, Value value); + Value Get(const std::string& key) const; + bool Has(const std::string& key) const; + void Erase(const std::string& key); + void Clear(); + + // Snapshot all keys for enumeration (avoids holding the lock during + // iteration on the JS side) + std::map Snapshot() const; + +private: + mutable std::mutex mtx; + std::map data; +}; diff --git a/skyrim-platform/src/platform_se/skyrim_platform/JsEngine.cpp b/skyrim-platform/src/platform_se/skyrim_platform/JsEngine.cpp index e6734b8b7..3f728d4a7 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/JsEngine.cpp +++ b/skyrim-platform/src/platform_se/skyrim_platform/JsEngine.cpp @@ -1,7 +1,39 @@ #include "JsEngine.h" #include +#include +#include #include "NodeInstance.h" +#include "ScopedTask.h" + +namespace { +enum V8ScriptId : uint16_t +{ + V8SCRIPT_INIT_SKYRIM_PLATFORM, + V8SCRIPT_CALL_PREPARED_FUNCTION, +}; + +constexpr auto kInitSkyrimPlatformScript = R"( + try { + const nodeProcess = require('node:process'); + const module = { exports: {} }; + + nodeProcess.dlopen(module, 'Data/Platform/Distribution/RuntimeDependencies/SkyrimPlatformImpl.dll'); + + globalThis.skyrimPlatformNativeAddon = module.exports; + } catch (e) { + reportError(e.toString()); + } + )"; + +constexpr auto kCallPreparedFunctionSсript = R"( + try { + skyrimPlatformNativeAddon.callPreparedFunction(); + } catch (e) { + reportError(e.toString()); + } + )"; +} struct JsEngine::Impl { @@ -9,12 +41,16 @@ struct JsEngine::Impl std::vector argv; int argc = 0; - char nodejsArgv0[5] = "node"; std::unique_ptr nodeInstance; + std::stack> preparedFunctionsStack; + int depth = 0; + std::optional retrievedEnv; + + v8::Isolate* isolate = nullptr; - std::function preparedFunction; + std::recursive_mutex m; }; JsEngine* JsEngine::GetSingleton() @@ -29,30 +65,39 @@ JsEngine::~JsEngine() delete pImpl; } -void JsEngine::AcquireEnvAndCall(const std::function& f) +void JsEngine::AcquireEnvAndCall(const std::function& f, + const char* comment) { - // spdlog::info("JsEngine::AcquireEnvAndCall()"); + std::lock_guard lock(pImpl->m); - if (!pImpl->nodeInstance) { - spdlog::error("JsEngine::AcquireEnvAndCall() - NodeInstance is nullptr"); + if (!pImpl->isolate) { + spdlog::error("JsEngine::AcquireEnvAndCall() - V8 Isolate is nullptr"); return; } - // napi_env env = reinterpret_cast(pImpl->env); - // Napi::Env cppEnv = Napi::Env(env); - // f(cppEnv); + v8::Locker locker(pImpl->isolate); - pImpl->preparedFunction = f; + Viet::ScopedTask task([](int& depth) { depth--; }, pImpl->depth); + pImpl->depth++; + + Viet::ScopedTask>> task2( + [](auto& preparedFunctionsStack) { preparedFunctionsStack.pop(); }, + pImpl->preparedFunctionsStack); + pImpl->preparedFunctionsStack.push(f); + + if (pImpl->depth > 1) { + return f(*pImpl->retrievedEnv); + } + + if (!pImpl->nodeInstance) { + spdlog::error("JsEngine::AcquireEnvAndCall() - NodeInstance is nullptr"); + return; + } pImpl->nodeInstance->ClearJavaScriptError(); - int executeScriptResult = pImpl->nodeInstance->ExecuteScript(pImpl->env, R"( - try { - skyrimPlatformNativeAddon.callPreparedFunction(); - } catch (e) { - reportError(e.toString()) - } - )"); + int executeScriptResult = pImpl->nodeInstance->ExecuteScript( + pImpl->env, V8SCRIPT_CALL_PREPARED_FUNCTION); if (executeScriptResult != 0) { spdlog::error( @@ -70,8 +115,6 @@ void JsEngine::AcquireEnvAndCall(const std::function& f) javaScriptError); throw std::runtime_error(javaScriptError); } - - // spdlog::info("JsEngine::AcquireEnvAndCall() - Leave"); } Napi::Value JsEngine::RunScript(Napi::Env env, const std::string& src, @@ -89,13 +132,20 @@ void JsEngine::ResetContext(Viet::TaskQueue&) size_t JsEngine::GetMemoryUsage() const { - spdlog::info("JsEngine::GetMemoryUsage()"); - // TODO - return 0; + if (!pImpl->isolate) { + return 0; + } + + v8::Locker locker(pImpl->isolate); + v8::HeapStatistics stats; + pImpl->isolate->GetHeapStatistics(&stats); + return stats.used_heap_size(); } void JsEngine::Tick() { + std::lock_guard lock(pImpl->m); + if (!pImpl->nodeInstance) { spdlog::error("JsEngine::Tick() - NodeInstance is nullptr"); return; @@ -106,6 +156,12 @@ void JsEngine::Tick() return; } + if (!pImpl->isolate) { + spdlog::error("JsEngine::Tick() - V8 Isolate is nullptr"); + return; + } + + v8::Locker locker(pImpl->isolate); pImpl->nodeInstance->Tick(pImpl->env); } @@ -145,24 +201,54 @@ JsEngine::JsEngine() spdlog::info("JsEngine::JsEngine() - Environment created"); - spdlog::info("JsEngine::JsEngine() - Executing script"); + pImpl->isolate = + static_cast(pImpl->nodeInstance->GetIsolatePtr(pImpl->env)); + if (!pImpl->isolate) { + spdlog::error("JsEngine::JsEngine() - Failed to get V8 Isolate"); + pImpl->nodeInstance.reset(); + return; + } + + // Activate v8::Locker mode for this isolate. From this point on, all V8 + // access must go through v8::Locker. This is required for thread-safe + // access to the V8 isolate from multiple threads (e.g. hook callbacks). + v8::Locker locker(pImpl->isolate); + + spdlog::info( + "JsEngine::JsEngine() - Copmpiling script V8SCRIPT_INIT_SKYRIM_PLATFORM"); pImpl->nodeInstance->ClearJavaScriptError(); - int executeResult = pImpl->nodeInstance->ExecuteScript(pImpl->env, R"( - try { - const nodeProcess = require('node:process'); - const module = { exports: {} }; - //require('./scam_native.node'); - nodeProcess.dlopen(module, 'Data/Platform/Distribution/RuntimeDependencies/SkyrimPlatformImpl.dll'); + int compileResult = pImpl->nodeInstance->CompileScript( + pImpl->env, kInitSkyrimPlatformScript, V8SCRIPT_INIT_SKYRIM_PLATFORM); + if (compileResult != 0) { + spdlog::error("JsEngine::JsEngine() - Failed to compile script: {}", + GetError()); + pImpl->nodeInstance.reset(); + return; + } - //module.exports.helloAddon(); + spdlog::info("JsEngine::JsEngine() - Copmpiling script " + "V8SCRIPT_CALL_PREPARED_FUNCTION"); - globalThis.skyrimPlatformNativeAddon = module.exports; - } catch (e) { - reportError(e.toString()) - } - )"); + pImpl->nodeInstance->ClearJavaScriptError(); + + compileResult = pImpl->nodeInstance->CompileScript( + pImpl->env, kCallPreparedFunctionSсript, V8SCRIPT_CALL_PREPARED_FUNCTION); + if (compileResult != 0) { + spdlog::error("JsEngine::JsEngine() - Failed to compile script: {}", + GetError()); + pImpl->nodeInstance.reset(); + return; + } + + spdlog::info( + "JsEngine::JsEngine() - Executing script V8SCRIPT_INIT_SKYRIM_PLATFORM"); + + pImpl->nodeInstance->ClearJavaScriptError(); + + int executeResult = pImpl->nodeInstance->ExecuteScript( + pImpl->env, V8SCRIPT_INIT_SKYRIM_PLATFORM); if (executeResult != 0) { spdlog::error("JsEngine::JsEngine() - Failed to execute script: {}", @@ -172,7 +258,10 @@ JsEngine::JsEngine() } std::string javaScriptError = pImpl->nodeInstance->GetJavaScriptError(); - spdlog::info("JsEngine::JsEngine() - JavaScript error: {}", javaScriptError); + if (!javaScriptError.empty()) { + spdlog::info("JsEngine::JsEngine() - JavaScript error: {}", + javaScriptError); + } pImpl->nodeInstance->ClearJavaScriptError(); spdlog::info("JsEngine::JsEngine() - Script executed"); @@ -195,10 +284,9 @@ std::string JsEngine::GetError() Napi::Value CallPreparedFunction(const Napi::CallbackInfo& info) { - // spdlog::info("CallPreparedFunction()"); + JsEngine::GetSingleton()->pImpl->retrievedEnv = info.Env(); - auto f = JsEngine::GetSingleton()->pImpl->preparedFunction; - JsEngine::GetSingleton()->pImpl->preparedFunction = nullptr; + auto f = JsEngine::GetSingleton()->pImpl->preparedFunctionsStack.top(); if (f) { f(info.Env()); diff --git a/skyrim-platform/src/platform_se/skyrim_platform/JsEngine.h b/skyrim-platform/src/platform_se/skyrim_platform/JsEngine.h index 6d02994f6..ba193f916 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/JsEngine.h +++ b/skyrim-platform/src/platform_se/skyrim_platform/JsEngine.h @@ -25,7 +25,10 @@ class JsEngine ~JsEngine(); - void AcquireEnvAndCall(const std::function& f); + // TODO: consider optimizing std::function out by using a C style callback + // and a void* context + void AcquireEnvAndCall(const std::function& f, + const char* comment); Napi::Value RunScript(Napi::Env env, const std::string& src, const std::string&); void ResetContext(Viet::TaskQueue&); diff --git a/skyrim-platform/src/platform_se/skyrim_platform/NodeInstance.cpp b/skyrim-platform/src/platform_se/skyrim_platform/NodeInstance.cpp index 835bb2488..2515d7bd3 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/NodeInstance.cpp +++ b/skyrim-platform/src/platform_se/skyrim_platform/NodeInstance.cpp @@ -48,9 +48,9 @@ void RegisterReportError(Isolate* isolate, Local context) struct NodeInstance::Impl { - std::map> createParamsMap; std::map isolatesMap; std::map> contextsMap; + std::vector>> compiledScripts; std::unique_ptr platform; std::string error; }; @@ -96,29 +96,14 @@ int NodeInstance::Init(int argc, char** argv) int NodeInstance::CreateEnvironment(int argc, char** argv, void** outEnv) { - // Create a v8::Platform instance. `MultiIsolatePlatform::Create()` is a way - // to create a v8::Platform instance that Node.js can use when creating - // Worker threads. When no `MultiIsolatePlatform` instance is present, - // Worker threads are disabled. pImpl->platform = MultiIsolatePlatform::Create(4); V8::InitializePlatform(pImpl->platform.get()); V8::Initialize(); - // Setup V8 isolate and context - // auto create_params = std::make_shared(); - // create_params->array_buffer_allocator = - // v8::ArrayBuffer::Allocator::NewDefaultAllocator(); - // Isolate* isolate = Isolate::New(*create_params); - Isolate* isolate = Isolate::Allocate(); - pImpl->platform->RegisterIsolate(isolate, uv_default_loop()); - auto create_params = std::make_shared(); - std::shared_ptr allocator = node::ArrayBufferAllocator::Create(); - isolate = NewIsolate(allocator, uv_default_loop(), pImpl->platform.get()); - - // register the isolate with the platform - // platform->RegisterIsolate(isolate, uv_default_loop()); + Isolate* isolate = + NewIsolate(allocator, uv_default_loop(), pImpl->platform.get()); { // Setup scope and context @@ -145,7 +130,6 @@ int NodeInstance::CreateEnvironment(int argc, char** argv, void** outEnv) "+ '/'); globalThis.require = publicRequire;", nullptr); - pImpl->createParamsMap[env] = create_params; pImpl->contextsMap[env].Reset(isolate, context); // Promote to Persistent and store pImpl->isolatesMap[env] = isolate; @@ -179,17 +163,6 @@ int NodeInstance::DestroyEnvironment(void* env) isolate = nullptr; // Null out the reference to avoid dangling pointers } - // Step 3: Optionally close the libuv loop if you're using one - // For example: - // uv_loop_close(uv_default_loop()); // Only if you created your own loop - - auto create_params = pImpl->createParamsMap[env]; - - if (create_params) { - delete create_params->array_buffer_allocator; - } - - pImpl->createParamsMap.erase(env); pImpl->contextsMap.erase(env); pImpl->isolatesMap.erase(env); @@ -218,15 +191,11 @@ int NodeInstance::Tick(void* env) return 0; // Success } -int NodeInstance::ExecuteScript(void* env, const char* script) +int NodeInstance::CompileScript(void* env, const char* script, + uint16_t scriptId) { - if (!env) { - pImpl->error = "No env"; - return -1; - } - - if (!script) { - pImpl->error = "No script"; + if (!env || !script) { + pImpl->error = "Invalid arguments"; return -1; } @@ -243,21 +212,60 @@ int NodeInstance::ExecuteScript(void* env, const char* script) Local context = contextPersistent.Get(isolate); Context::Scope context_scope(context); + TryCatch try_catch(isolate); + Local source = String::NewFromUtf8(isolate, script).ToLocalChecked(); Local