/**************************************************************************** Copyright (c) 2016 Chukong Technologies Inc. Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd. http://www.cocos.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ****************************************************************************/ #include "ScriptEngine.h" #if SCRIPT_ENGINE_TYPE == SCRIPT_ENGINE_SM #include "../MappingUtils.h" #include "../State.h" #include "Class.h" #include "Object.h" #include "Utils.h" // for debug socket #ifdef _WIN32 #include #include #else #include #include #include #endif #include #include #include #if SE_DEBUG #define TRACE_DEBUGGER_SERVER(...) SE_LOGD(__VA_ARGS__) #else #define TRACE_DEBUGGER_SERVER(...) #endif // #if CC_DEBUG uint32_t __jsbInvocationCount = 0; namespace se { namespace { const char *BYTE_CODE_FILE_EXT = ".jsc"; ScriptEngine *__instance = nullptr; const JSClassOps global_classOps = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve nullptr, // finalize nullptr, // call nullptr, // hasInstance nullptr, // construct JS_GlobalObjectTraceHook, // trace }; JSClass __globalClass = { "global", JSCLASS_GLOBAL_FLAGS, &global_classOps}; void reportWarning(JSContext *cx, JSErrorReport *report) { MOZ_RELEASE_ASSERT(report); // MOZ_RELEASE_ASSERT(report->isWarning()); SE_LOGE("%s:%u:%s\n", report->filename ? report->filename : "", (unsigned int)report->lineno, report->message().c_str()); } void SetStandardCompartmentOptions(JS::RealmOptions &options) { bool enableSharedMemory = true; options.creationOptions().setSharedMemoryAndAtomicsEnabled(enableSharedMemory); } bool __forceGC(JSContext *cx, uint32_t argc, JS::Value *vp) { JS_GC(cx); return true; } bool __log(JSContext *cx, uint32_t argc, JS::Value *vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (argc > 0) { JSString *string = JS::ToString(cx, args[0]); if (string) { JS::RootedString jsstr(cx, string); JS::UniqueChars buffer = JS_EncodeStringToUTF8(cx, jsstr); SE_LOGD("JS: %s\n", buffer.get()); } } args.rval().setUndefined(); return true; } // Private data class bool privateDataContructor(JSContext *cx, uint32_t argc, JS::Value *vp) { return true; } void privateDataFinalize(JSFreeOp *fop, JSObject *obj) { internal::PrivateData *p = (internal::PrivateData *)internal::SE_JS_GetPrivate(obj, 0); if (p == nullptr) return; internal::SE_JS_SetPrivate(obj, 0, p->data); if (p->finalizeCb != nullptr) p->finalizeCb(fop, obj); free(p); } // ------------------------------------------------------- ScriptEngine void on_garbage_collect(JSContext *cx, JSGCStatus status, JS::GCReason reason, void *data) { /* We finalize any pending toggle refs before doing any garbage collection, * so that we can collect the JS wrapper objects, and in order to minimize * the chances of objects having a pending toggle up queued when they are * garbage collected. */ if (status == JSGC_BEGIN) { ScriptEngine::getInstance()->_setGarbageCollecting(true); SE_LOGD("on_garbage_collect: begin, Native -> JS map count: %d, all objects: %d\n", (int)NativePtrToObjectMap::size(), (int)__objectMap.size()); } else if (status == JSGC_END) { SE_LOGD("on_garbage_collect: end, Native -> JS map count: %d, all objects: %d\n", (int)NativePtrToObjectMap::size(), (int)__objectMap.size()); ScriptEngine::getInstance()->_setGarbageCollecting(false); } } std::string removeFileExt(const std::string &filePath) { size_t pos = filePath.rfind('.'); if (0 < pos) { return filePath.substr(0, pos); } return filePath; } bool getBytecodeBuildId(JS::BuildIdCharVector *buildId) { // The browser embeds the date into the buildid and the buildid is embedded // in the binary, so every 'make' necessarily builds a new firefox binary. // Fortunately, the actual firefox executable is tiny -- all the code is in // libxul.so and other shared modules -- so this isn't a big deal. Not so // for the statically-linked JS shell. To avoid recompiling js.cpp and // re-linking 'js' on every 'make', we use a constant buildid and rely on // the shell user to manually clear any caches between cache-breaking updates. const char buildid[] = "cocos_xdr"; return buildId->append(buildid, sizeof(buildid)); } // For console stuff bool JSB_console_format_log(State &s, const char *prefix, int msgIndex = 0) { if (msgIndex < 0) return false; const auto &args = s.args(); int argc = (int)args.size(); if ((argc - msgIndex) == 1) { std::string msg = args[msgIndex].toStringForce(); SE_LOGD("JS: %s%s\n", prefix, msg.c_str()); } else if (argc > 1) { std::string msg = args[msgIndex].toStringForce(); size_t pos; for (int i = (msgIndex + 1); i < argc; ++i) { pos = msg.find("%"); if (pos != std::string::npos && pos != (msg.length() - 1) && (msg[pos + 1] == 'd' || msg[pos + 1] == 's' || msg[pos + 1] == 'f')) { msg.replace(pos, 2, args[i].toStringForce()); } else { msg += " " + args[i].toStringForce(); } } SE_LOGD("JS: %s%s\n", prefix, msg.c_str()); } return true; } bool JSB_console_log(State &s) { JSB_console_format_log(s, ""); return true; } SE_BIND_FUNC(JSB_console_log) bool JSB_console_debug(State &s) { JSB_console_format_log(s, "[DEBUG]: "); return true; } SE_BIND_FUNC(JSB_console_debug) bool JSB_console_info(State &s) { JSB_console_format_log(s, "[INFO]: "); return true; } SE_BIND_FUNC(JSB_console_info) bool JSB_console_warn(State &s) { JSB_console_format_log(s, "[WARN]: "); return true; } SE_BIND_FUNC(JSB_console_warn) bool JSB_console_error(State &s) { JSB_console_format_log(s, "[ERROR]: "); return true; } SE_BIND_FUNC(JSB_console_error) bool JSB_console_assert(State &s) { const auto &args = s.args(); if (!args.empty()) { if (args[0].isBoolean() && !args[0].toBoolean()) { JSB_console_format_log(s, "[ASSERT]: ", 1); } } return true; } SE_BIND_FUNC(JSB_console_assert) bool JSB_console_time(State &s) { return true; //TODO(cjh) } SE_BIND_FUNC(JSB_console_time) bool JSB_console_timeEnd(State &s) { return true; //TODO(cjh) } SE_BIND_FUNC(JSB_console_timeEnd) } // namespace AutoHandleScope::AutoHandleScope() { } AutoHandleScope::~AutoHandleScope() { js::RunJobs(se::ScriptEngine::getInstance()->_getContext()); } ScriptEngine *ScriptEngine::getInstance() { if (__instance == nullptr) { __instance = ccnew ScriptEngine(); } return __instance; } void ScriptEngine::destroyInstance() { delete __instance; __instance = nullptr; } ScriptEngine::ScriptEngine() : _cx(nullptr), _globalObj(nullptr), _debugGlobalObj(nullptr), _oldCompartment(nullptr), _exceptionCallback(nullptr), _debuggerServerPort(0), _vmId(0), _isGarbageCollecting(false), _isValid(false), _isInCleanup(false), _isErrorHandleWorking(false) { bool ok = JS_Init(); assert(ok); } /* static */ void ScriptEngine::onWeakPointerCompartmentCallback(JSTracer *trc, JS::Compartment *comp, void *data) { onWeakPointerZoneGroupCallback(trc, data); } /* static */ void ScriptEngine::onWeakPointerZoneGroupCallback(JSTracer *trc, void *data) { bool isInCleanup = getInstance()->isInCleanup(); bool isIterUpdated = false; Object *obj = nullptr; auto iter = NativePtrToObjectMap::begin(); while (iter != NativePtrToObjectMap::end()) { obj = iter->second; isIterUpdated = false; if (!obj->isRooted()) { if (obj->updateAfterGC(trc, data)) { obj->decRef(); iter = NativePtrToObjectMap::erase(iter); isIterUpdated = true; } } else if (isInCleanup) // Rooted and in cleanup step { obj->unprotect(); obj->decRef(); iter = NativePtrToObjectMap::erase(iter); isIterUpdated = true; } if (!isIterUpdated) ++iter; } } bool ScriptEngine::init() { cleanup(); SE_LOGD("Initializing SpiderMonkey, version: %s\n", JS_GetImplementationVersion()); ++_vmId; for (const auto &hook : _beforeInitHookArray) { hook(); } _beforeInitHookArray.clear(); _cx = JS_NewContext(JS::DefaultHeapMaxBytes); if (nullptr == _cx) return false; NativePtrToObjectMap::init(); Class::setContext(_cx); Object::setContext(_cx); js::UseInternalJobQueues(_cx); JS_SetGCParameter(_cx, JSGC_MAX_BYTES, 0xffffffff); JS_SetGCParameter(_cx, JSGC_INCREMENTAL_GC_ENABLED, 1); JS_SetGCParameter(_cx, JSGC_PER_ZONE_GC_ENABLED, 1); JS_SetGCParameter(_cx, JSGC_COMPACTING_ENABLED, 1); JS_SetNativeStackQuota(_cx, 5000000); JS_SetOffthreadIonCompilationEnabled(_cx, true); JS_SetGlobalJitCompilerOption(_cx, JSJITCOMPILER_ION_ENABLE, 1); JS_SetGlobalJitCompilerOption(_cx, JSJITCOMPILER_BASELINE_ENABLE, 1); JS_SetGCCallback(_cx, on_garbage_collect, nullptr); if (!JS::InitSelfHostedCode(_cx)) return false; // Waiting is allowed on the shell's main thread, for now. JS_SetFutexCanWait(_cx); JS::SetWarningReporter(_cx, reportWarning); #if defined(JS_GC_ZEAL) && defined(DEBUG) // JS_SetGCZeal(_cx, 2, JS_DEFAULT_ZEAL_FREQ); #endif JS::RealmOptions options; SetStandardCompartmentOptions(options); #ifdef DEBUG JS::ContextOptionsRef(_cx) .setDisableIon() .setWasmBaseline(false) .setAsmJS(false) .setWasm(false) .setWasmIon(false); #else JS::ContextOptionsRef(_cx) .setAsmJS(true) .setWasm(true) .setWasmBaseline(true) .setWasmIon(true); #endif JS::RootedObject globalObj(_cx, JS_NewGlobalObject(_cx, &__globalClass, nullptr, JS::DontFireOnNewGlobalHook, options)); if (nullptr == globalObj) return false; _globalObj = Object::_createJSObject(nullptr, globalObj); _globalObj->root(); JS::RootedObject rootedGlobalObj(_cx, _globalObj->_getJSObject()); _oldCompartment = JS::EnterRealm(_cx, rootedGlobalObj); JS::InitRealmStandardClasses(_cx); _globalObj->setProperty("window", Value(_globalObj)); // SpiderMonkey isn't shipped with a console variable. Make a fake one. Value consoleVal; bool hasConsole = _globalObj->getProperty("console", &consoleVal) && consoleVal.isObject(); assert(!hasConsole); HandleObject consoleObj(Object::createPlainObject()); consoleObj->defineFunction("log", _SE(JSB_console_log)); consoleObj->defineFunction("debug", _SE(JSB_console_debug)); consoleObj->defineFunction("info", _SE(JSB_console_info)); consoleObj->defineFunction("warn", _SE(JSB_console_warn)); consoleObj->defineFunction("error", _SE(JSB_console_error)); consoleObj->defineFunction("assert", _SE(JSB_console_assert)); consoleObj->defineFunction("time", _SE(JSB_console_info)); //TODO(cjh) consoleObj->defineFunction("timeEnd", _SE(JSB_console_info)); //TODO(cjh) _globalObj->setProperty("console", Value(consoleObj)); _globalObj->setProperty("scriptEngineType", Value("SpiderMonkey")); JS_DefineFunction(_cx, rootedGlobalObj, "log", __log, 0, JSPROP_PERMANENT); JS_DefineFunction(_cx, rootedGlobalObj, "forceGC", __forceGC, 0, JSPROP_READONLY | JSPROP_PERMANENT); JS_FireOnNewGlobalObject(_cx, rootedGlobalObj); JS::SetProcessBuildIdOp(getBytecodeBuildId); _isValid = true; for (const auto &hook : _afterInitHookArray) { hook(); } _afterInitHookArray.clear(); return true; } ScriptEngine::~ScriptEngine() { cleanup(); JS_ShutDown(); } void ScriptEngine::cleanup() { if (!_isValid) return; _isInCleanup = true; for (const auto &hook : _beforeCleanupHookArray) { hook(); } _beforeCleanupHookArray.clear(); SAFE_DEC_REF(_globalObj); Class::cleanup(); Object::cleanup(); JS::LeaveRealm(_cx, _oldCompartment); JS_DestroyContext(_cx); _cx = nullptr; _globalObj = nullptr; _oldCompartment = nullptr; _isValid = false; _registerCallbackArray.clear(); _filenameScriptMap.clear(); for (const auto &hook : _afterCleanupHookArray) { hook(); } _afterCleanupHookArray.clear(); _isInCleanup = false; NativePtrToObjectMap::destroy(); } void ScriptEngine::addBeforeCleanupHook(const std::function &hook) { _beforeCleanupHookArray.push_back(hook); } void ScriptEngine::addAfterCleanupHook(const std::function &hook) { _afterCleanupHookArray.push_back(hook); } void ScriptEngine::addBeforeInitHook(const std::function &hook) { _beforeInitHookArray.push_back(hook); } void ScriptEngine::addAfterInitHook(const std::function &hook) { _afterInitHookArray.push_back(hook); } bool ScriptEngine::isGarbageCollecting() { return _isGarbageCollecting; } void ScriptEngine::_setGarbageCollecting(bool isGarbageCollecting) { _isGarbageCollecting = isGarbageCollecting; } Object *ScriptEngine::getGlobalObject() { return _globalObj; } void ScriptEngine::addRegisterCallback(RegisterCallback cb) { assert(std::find(_registerCallbackArray.begin(), _registerCallbackArray.end(), cb) == _registerCallbackArray.end()); _registerCallbackArray.push_back(cb); } void ScriptEngine::addPermanentRegisterCallback(RegisterCallback cb) { if (std::find(_permRegisterCallbackArray.begin(), _permRegisterCallbackArray.end(), cb) == _permRegisterCallbackArray.end()) { _permRegisterCallbackArray.push_back(cb); } } #pragma mark - Debug static std::string inData; static std::string outData; static ccstd::vector g_queue; static std::mutex g_qMutex; static std::mutex g_rwMutex; static int clientSocket = -1; static uint32_t s_nestedLoopLevel = 0; static void cc_closesocket(int fd) { #ifdef _WIN32 closesocket(fd); #else close(fd); #endif } void ScriptEngine::_debugProcessInput(const std::string &str) { JS::RootedObject debugGlobal(_cx, _debugGlobalObj->_getJSObject()); JS::Realm *globalCpt = JS::EnterRealm(_cx, debugGlobal); Value func; if (_debugGlobalObj->getProperty("processInput", &func) && func.isObject() && func.toObject()->isFunction()) { ValueArray args; args.push_back(Value(str)); func.toObject()->call(args, _debugGlobalObj); } JS::LeaveRealm(_cx, globalCpt); } static bool NS_ProcessNextEvent() { std::string message; size_t messageCount = 0; while (true) { g_qMutex.lock(); messageCount = g_queue.size(); if (messageCount > 0) { auto first = g_queue.begin(); message = *first; g_queue.erase(first); --messageCount; } g_qMutex.unlock(); if (!message.empty()) { ScriptEngine::getInstance()->_debugProcessInput(message); } if (messageCount == 0) break; } // std::this_thread::yield(); std::this_thread::sleep_for(std::chrono::milliseconds(10)); return true; } static bool JSBDebug_enterNestedEventLoop(State &s) { enum { NS_OK = 0, NS_ERROR_UNEXPECTED }; #define NS_SUCCEEDED(v) ((v) == NS_OK) int rv = NS_OK; uint32_t nestLevel = ++s_nestedLoopLevel; while (NS_SUCCEEDED(rv) && s_nestedLoopLevel >= nestLevel) { js::RunJobs(se::ScriptEngine::getInstance()->_getContext()); if (!NS_ProcessNextEvent()) rv = NS_ERROR_UNEXPECTED; } assert(s_nestedLoopLevel <= nestLevel); s.rval().setInt32(s_nestedLoopLevel); return true; } SE_BIND_FUNC(JSBDebug_enterNestedEventLoop) static bool JSBDebug_exitNestedEventLoop(State &s) { if (s_nestedLoopLevel > 0) { --s_nestedLoopLevel; } else { s.rval().setInt32(0); return true; } return true; } SE_BIND_FUNC(JSBDebug_exitNestedEventLoop) static bool JSBDebug_getEventLoopNestLevel(State &s) { s.rval().setInt32(s_nestedLoopLevel); return true; } SE_BIND_FUNC(JSBDebug_getEventLoopNestLevel) static void _clientSocketWriteAndClearString(std::string &s) { ::send(clientSocket, s.c_str(), s.length(), 0); s.clear(); } static void processInput(const std::string &data) { std::lock_guard lk(g_qMutex); g_queue.push_back(data); } static void clearBuffers() { std::lock_guard lk(g_rwMutex); // only process input if there's something and we're not locked if (!inData.empty()) { processInput(inData); inData.clear(); } if (!outData.empty()) { _clientSocketWriteAndClearString(outData); } } static void serverEntryPoint(uint32_t port) { // start a server, accept the connection and keep reading data from it struct addrinfo hints, *result = nullptr, *rp = nullptr; int s = 0; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; // TCP stream sockets hints.ai_flags = AI_PASSIVE; // fill in my IP for me std::stringstream portstr; portstr << port; int err = 0; #ifdef _WIN32 WSADATA wsaData; err = WSAStartup(MAKEWORD(2, 2), &wsaData); #endif if ((err = getaddrinfo(NULL, portstr.str().c_str(), &hints, &result)) != 0) { SE_LOGD("getaddrinfo error : %s\n", gai_strerror(err)); } for (rp = result; rp != NULL; rp = rp->ai_next) { if ((s = socket(rp->ai_family, rp->ai_socktype, 0)) < 0) { continue; } int optval = 1; if ((setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&optval, sizeof(optval))) < 0) { cc_closesocket(s); TRACE_DEBUGGER_SERVER("debug server : error setting socket option SO_REUSEADDR\n"); return; } #ifdef __APPLE__ if ((setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval))) < 0) { close(s); TRACE_DEBUGGER_SERVER("debug server : error setting socket option SO_NOSIGPIPE\n"); return; } #endif if ((::bind(s, rp->ai_addr, rp->ai_addrlen)) == 0) { break; } cc_closesocket(s); s = -1; } if (s < 0 || rp == NULL) { TRACE_DEBUGGER_SERVER("debug server : error creating/binding socket\n"); return; } freeaddrinfo(result); listen(s, 1); #define MAX_RECEIVED_SIZE 1024 #define BUF_SIZE MAX_RECEIVED_SIZE + 1 char buf[BUF_SIZE] = {0}; int readBytes = 0; while (true) { clientSocket = accept(s, NULL, NULL); if (clientSocket < 0) { TRACE_DEBUGGER_SERVER("debug server : error on accept\n"); return; } else { // read/write data TRACE_DEBUGGER_SERVER("debug server : client connected\n"); inData = "connected"; // process any input, send any output clearBuffers(); while ((readBytes = (int)::recv(clientSocket, buf, MAX_RECEIVED_SIZE, 0)) > 0) { buf[readBytes] = '\0'; // TRACE_DEBUGGER_SERVER("debug server : received command >%s", buf); // no other thread is using this inData.append(buf); // process any input, send any output clearBuffers(); } // while(read) cc_closesocket(clientSocket); } } // while(true) #undef BUF_SIZE #undef MAX_RECEIVED_SIZE } static bool JSBDebug_require(State &s) { const auto &args = s.args(); int argc = (int)args.size(); if (argc >= 1) { ScriptEngine::getInstance()->runScript(args[0].toString()); return true; } SE_REPORT_ERROR("Wrong number of arguments: %d, expected: %d", argc, 1); return false; } SE_BIND_FUNC(JSBDebug_require) static bool JSBDebug_BufferWrite(State &s) { const auto &args = s.args(); int argc = (int)args.size(); if (argc == 1) { // this is safe because we're already inside a lock (from clearBuffers) outData.append(args[0].toString()); _clientSocketWriteAndClearString(outData); } return true; } SE_BIND_FUNC(JSBDebug_BufferWrite) bool ScriptEngine::start() { if (!init()) return false; if (isDebuggerEnabled() && _debugGlobalObj == nullptr) { JS::RealmOptions options; options.creationOptions().setSharedMemoryAndAtomicsEnabled(true); JS::RootedObject debugGlobal(_cx, JS_NewGlobalObject(_cx, &__globalClass, nullptr, JS::DontFireOnNewGlobalHook, options)); _debugGlobalObj = Object::_createJSObject(nullptr, debugGlobal); _debugGlobalObj->root(); JS::Realm *globalCpt = JS::EnterRealm(_cx, debugGlobal); JS::InitRealmStandardClasses(_cx); JS_FireOnNewGlobalObject(_cx, debugGlobal); JS_DefineDebuggerObject(_cx, debugGlobal); // these are used in the debug program JS_DefineFunction(_cx, debugGlobal, "log", __log, 0, JSPROP_PERMANENT); _debugGlobalObj->defineFunction("require", _SE(JSBDebug_require)); _debugGlobalObj->defineFunction("_bufferWrite", _SE(JSBDebug_BufferWrite)); _debugGlobalObj->defineFunction("_enterNestedEventLoop", _SE(JSBDebug_enterNestedEventLoop)); _debugGlobalObj->defineFunction("_exitNestedEventLoop", _SE(JSBDebug_exitNestedEventLoop)); _debugGlobalObj->defineFunction("_getEventLoopNestLevel", _SE(JSBDebug_getEventLoopNestLevel)); JS::RootedObject globalObj(_cx, _globalObj->_getJSObject()); JS_WrapObject(_cx, &globalObj); runScript("script/jsb_debugger.js"); // prepare the debugger Value prepareDebuggerFunc; assert(_debugGlobalObj->getProperty("_prepareDebugger", &prepareDebuggerFunc) && prepareDebuggerFunc.isObject() && prepareDebuggerFunc.toObject()->isFunction()); ValueArray args; args.push_back(Value(_globalObj)); prepareDebuggerFunc.toObject()->call(args, _debugGlobalObj); // start bg thread auto t = std::thread(&serverEntryPoint, _debuggerServerPort); t.detach(); JS::LeaveRealm(_cx, globalCpt); } bool ok = false; _startTime = std::chrono::steady_clock::now(); for (auto cb : _permRegisterCallbackArray) { ok = cb(_globalObj); assert(ok); if (!ok) { break; } } for (auto cb : _registerCallbackArray) { ok = cb(_globalObj); assert(ok); if (!ok) break; } // After ScriptEngine is started, _registerCallbackArray isn't needed. Therefore, clear it here. _registerCallbackArray.clear(); return ok; } bool ScriptEngine::getScript(const std::string &path, JS::MutableHandleScript script) { std::string fullPath = _fileOperationDelegate.onGetFullPath(path); auto iter = _filenameScriptMap.find(fullPath); if (iter != _filenameScriptMap.end()) { JS::PersistentRootedScript *rootedScript = iter->second; script.set(rootedScript->get()); return true; } // // a) check jsc file first // std::string byteCodePath = removeFileExt(path) + BYTE_CODE_FILE_EXT; // if (_filenameScriptMap.find(byteCodePath) != _filenameScriptMap.end()) // { // script.set(_filenameScriptMap[byteCodePath]->get()); // return true; // } // // // b) no jsc file, check js file // if (_filenameScriptMap.find(path) != _filenameScriptMap.end()) // { // script.set(_filenameScriptMap[path]->get()); // return true; // } return false; } bool ScriptEngine::compileScript(const std::string &path, JS::MutableHandleScript script) { if (path.empty()) return false; bool ok = getScript(path, script); if (ok) return true; assert(_fileOperationDelegate.isValid()); bool compileSucceed = false; // Creator v1.7 supports v8, spidermonkey, javascriptcore and chakracore as its script engine, // jsc file isn't bytecode format anymore, it's a xxtea encrpted binary format instead. // Therefore, for unifying the flow, ScriptEngine class will not support spidermonkey bytecode. // // a) check jsc file first // std::string byteCodePath = removeFileExt(path) + BYTE_CODE_FILE_EXT; // // // Check whether '.jsc' files exist to avoid outputting log which says 'couldn't find .jsc file'. // if (_fileOperationDelegate.onCheckFileExist(byteCodePath)) // { // _fileOperationDelegate.onGetDataFromFile(byteCodePath, [&](const uint8_t* data, size_t dataLen) { // if (data != nullptr && dataLen > 0) // { // JS::TranscodeBuffer buffer; // bool appended = buffer.append(data, dataLen); // JS::TranscodeResult result = JS::DecodeScript(_cx, buffer, script); // if (appended && result == JS::TranscodeResult::TranscodeResult_Ok) // { // compileSucceed = true; // _filenameScriptMap[byteCodePath] = ccnew JS::PersistentRootedScript(_cx, script.get()); // } // assert(compileSucceed); // } // }); // // } // b) no jsc file, check js file if (!compileSucceed) { /* Clear any pending exception from previous failed decoding. */ clearException(); std::string jsFileContent = _fileOperationDelegate.onGetStringFromFile(path); if (!jsFileContent.empty()) { JS::CompileOptions op(_cx); op.setFileAndLine(path.c_str(), 1); JS::SourceText srcBuf; bool succeed = srcBuf.init(_cx, jsFileContent.c_str(), jsFileContent.length(), JS::SourceOwnership::Borrowed); assert(succeed); JSScript *compiledScript = JS::Compile(_cx, op, srcBuf); if (compiledScript != nullptr) { compileSucceed = true; script.set(compiledScript); std::string fullPath = _fileOperationDelegate.onGetFullPath(path); _filenameScriptMap[fullPath] = ccnew JS::PersistentRootedScript(_cx, script.get()); } assert(compileSucceed); } } clearException(); if (!compileSucceed) { SE_LOGD("ScriptEngine::compileScript fail:%s\n", path.c_str()); } return compileSucceed; } bool ScriptEngine::evalString(const char *script, ssize_t length /* = -1 */, Value *ret /* = nullptr */, const char *fileName /* = nullptr */) { assert(script != nullptr); if (length < 0) length = strlen(script); if (fileName == nullptr) fileName = "(no filename)"; JS::CompileOptions options(_cx); options.setFile(fileName); JS::RootedValue rval(_cx); JS::SourceText srcBuf; bool succeed = srcBuf.init(_cx, script, length, JS::SourceOwnership::Borrowed); assert(succeed); bool ok = JS::Evaluate(_cx, options, srcBuf, &rval); if (!ok) { clearException(); } assert(ok); if (ok && ret != nullptr && !rval.isNullOrUndefined()) { internal::jsToSeValue(_cx, rval, ret); } if (!ok) { SE_LOGE("ScriptEngine::evalString script %s, failed!\n", fileName); } return ok; } void ScriptEngine::setFileOperationDelegate(const FileOperationDelegate &delegate) { _fileOperationDelegate = delegate; } const ScriptEngine::FileOperationDelegate &ScriptEngine::getFileOperationDelegate() const { return _fileOperationDelegate; } bool ScriptEngine::runScript(const std::string &path, Value *ret /* = nullptr */) { assert(_fileOperationDelegate.isValid()); JS::RootedScript script(_cx); bool ok = compileScript(path, &script); if (ok) { JS::RootedValue rval(_cx); ok = JS_ExecuteScript(_cx, script, &rval); if (!ok) { SE_LOGE("Evaluating %s failed (evaluatedOK == JS_FALSE)\n", path.c_str()); clearException(); } if (ok && ret != nullptr && !rval.isNullOrUndefined()) { internal::jsToSeValue(_cx, rval, ret); } } return ok; } void ScriptEngine::clearException() { if (_cx == nullptr) return; if (JS_IsExceptionPending(_cx)) { JS::RootedValue exceptionValue(_cx); JS_GetPendingException(_cx, &exceptionValue); JS_ClearPendingException(_cx); assert(exceptionValue.isObject()); JS::RootedObject exceptionObj(_cx, exceptionValue.toObjectOrNull()); JSErrorReport *report = JS_ErrorFromException(_cx, exceptionObj); const char *message = report->message().c_str(); const std::string filePath = report->filename != nullptr ? report->filename : "(no filename)"; char line[50] = {0}; snprintf(line, sizeof(line), "%u", report->lineno); char column[50] = {0}; snprintf(column, sizeof(column), "%u", report->column); const std::string location = filePath + ":" + line + ":" + column; JS::UniqueChars stack; JS::RootedValue stackVal(_cx); if (JS_GetProperty(_cx, exceptionObj, "stack", &stackVal) && stackVal.isString()) { JS::RootedString jsstackStr(_cx, stackVal.toString()); stack = JS_EncodeStringToUTF8(_cx, jsstackStr); } std::string exceptionStr = message; exceptionStr += ", location: " + location; if (stack != nullptr) { exceptionStr += "\nSTACK:\n"; exceptionStr += stack.get(); } SE_LOGE("ERROR: %s\n", exceptionStr.c_str()); if (_exceptionCallback != nullptr) { _exceptionCallback(location.c_str(), message, stack.get()); } if (!_isErrorHandleWorking) { _isErrorHandleWorking = true; Value errorHandler; if (_globalObj->getProperty("__errorHandler", &errorHandler) && errorHandler.isObject() && errorHandler.toObject()->isFunction()) { ValueArray args; args.push_back(Value(filePath)); args.push_back(Value(report->lineno)); args.push_back(Value(message)); if (stack) { args.push_back(Value(stack.get())); } else { args.push_back(Value("")); } errorHandler.toObject()->call(args, _globalObj); } _isErrorHandleWorking = false; } else { SE_LOGE("ERROR: __errorHandler has exception\n"); } } } void ScriptEngine::setExceptionCallback(const ExceptionCallback &cb) { _exceptionCallback = cb; } void ScriptEngine::setJSExceptionCallback(const ExceptionCallback &cb) { //TODO(cjh) } void ScriptEngine::enableDebugger(const std::string &serverAddr, uint32_t port, bool isWait) { _debuggerServerAddr = serverAddr; _debuggerServerPort = port; } bool ScriptEngine::isDebuggerEnabled() const { return !_debuggerServerAddr.empty() && _debuggerServerPort > 0; } void ScriptEngine::mainLoopUpdate() { js::RunJobs(_cx); if (!isDebuggerEnabled()) { return; } std::string message; size_t messageCount = 0; while (true) { g_qMutex.lock(); messageCount = g_queue.size(); if (messageCount > 0) { auto first = g_queue.begin(); message = *first; g_queue.erase(first); --messageCount; } g_qMutex.unlock(); if (!message.empty()) { _debugProcessInput(message); } if (messageCount == 0) break; } } bool ScriptEngine::callFunction(Object *targetObj, const char *funcName, uint32_t argc, Value *args, Value *rval /* = nullptr*/) { JS::RootedValueVector jsarr(_cx); if (!jsarr.reserve(argc)) { SE_LOGE("ScriptEngine::callFunction out of memory!"); return false; } for (size_t i = 0; i < argc; ++i) { JS::RootedValue jsval(_cx); internal::seToJsValue(_cx, args[i], &jsval); jsarr.append(jsval); } JS::RootedObject contextObject(_cx); if (targetObj != nullptr) { contextObject.set(targetObj->_getJSObject()); } bool found = false; bool ok = JS_HasProperty(_cx, contextObject, funcName, &found); if (!ok || !found) { return false; } JS::RootedValue funcValue(_cx); ok = JS_GetProperty(_cx, contextObject, funcName, &funcValue); if (!ok) { return false; } JS::RootedValue rcValue(_cx); JSAutoRealm autoRealm(_cx, funcValue.toObjectOrNull()); ok = JS_CallFunctionValue(_cx, contextObject, funcValue, jsarr, &rcValue); if (ok) { if (rval != nullptr) internal::jsToSeValue(_cx, rcValue, rval); } else { se::ScriptEngine::getInstance()->clearException(); } return ok; } void ScriptEngine::handlePromiseExceptions() { clearException(); } //TODO(cjh) } // namespace se #endif // #if SCRIPT_ENGINE_TYPE == SCRIPT_ENGINE_SM