You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1150 lines
34 KiB
1150 lines
34 KiB
/****************************************************************************
|
|
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 <WS2tcpip.h>
|
|
#include <io.h>
|
|
#else
|
|
#include <netdb.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include <mutex>
|
|
#include <sstream>
|
|
#include <thread>
|
|
|
|
#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 : "<no 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<void()> &hook) {
|
|
_beforeCleanupHookArray.push_back(hook);
|
|
}
|
|
|
|
void ScriptEngine::addAfterCleanupHook(const std::function<void()> &hook) {
|
|
_afterCleanupHookArray.push_back(hook);
|
|
}
|
|
|
|
void ScriptEngine::addBeforeInitHook(const std::function<void()> &hook) {
|
|
_beforeInitHookArray.push_back(hook);
|
|
}
|
|
|
|
void ScriptEngine::addAfterInitHook(const std::function<void()> &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<std::string> 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<std::mutex> lk(g_qMutex);
|
|
g_queue.push_back(data);
|
|
}
|
|
|
|
static void clearBuffers() {
|
|
std::lock_guard<std::mutex> 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<mozilla::Utf8Unit> 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<mozilla::Utf8Unit> 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
|
|
|