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.
 
 
 
 
 
 

1254 lines
42 KiB

/****************************************************************************
Copyright (c) 2020-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"
#include "engine/EngineEvents.h"
#if SCRIPT_ENGINE_TYPE == SCRIPT_ENGINE_V8
#include "../MappingUtils.h"
#include "../State.h"
#include "Class.h"
#include "Object.h"
#include "Utils.h"
#include "base/Log.h"
#include "base/std/container/unordered_map.h"
#include "platform/FileUtils.h"
#include "plugins/bus/EventBus.h"
#include <sstream>
#if SE_ENABLE_INSPECTOR
#include "debugger/env.h"
#include "debugger/inspector_agent.h"
#include "debugger/node.h"
#endif
#include "base/std/container/array.h"
#define EXPOSE_GC "__jsb_gc__"
const unsigned int JSB_STACK_FRAME_LIMIT = 20;
#ifdef CC_DEBUG
unsigned int jsbInvocationCount = 0;
ccstd::unordered_map<ccstd::string, unsigned> jsbFunctionInvokedRecords;
#endif
#define RETRUN_VAL_IF_FAIL(cond, val) \
if (!(cond)) return val
namespace se {
AutoHandleScope::AutoHandleScope()
: _handleScope(v8::Isolate::GetCurrent()) {
#if CC_EDITOR
ScriptEngine::getInstance()->_getContext()->Enter();
#endif
}
AutoHandleScope::~AutoHandleScope() { // NOLINT
#if CC_EDITOR
ScriptEngine::getInstance()->_getContext()->Exit();
#endif
}
namespace {
void seLogCallback(const v8::FunctionCallbackInfo<v8::Value> &info) {
if (info[0]->IsString()) {
v8::String::Utf8Value utf8(v8::Isolate::GetCurrent(), info[0]);
cc::Log::logMessage(cc::LogType::KERNEL, cc::LogLevel::LEVEL_DEBUG
, "JS: %s", *utf8);
}
}
void seForceGC(const v8::FunctionCallbackInfo<v8::Value> & /*info*/) {
ScriptEngine::getInstance()->garbageCollect();
}
ccstd::string stackTraceToString(v8::Local<v8::StackTrace> stack) {
ccstd::string stackStr;
if (stack.IsEmpty()) {
return stackStr;
}
char tmp[100] = {0};
for (int i = 0, e = stack->GetFrameCount(); i < e; ++i) {
v8::Local<v8::StackFrame> frame = stack->GetFrame(v8::Isolate::GetCurrent(), i);
v8::Local<v8::String> script = frame->GetScriptName();
ccstd::string scriptName;
if (!script.IsEmpty()) {
scriptName = *v8::String::Utf8Value(v8::Isolate::GetCurrent(), script);
}
v8::Local<v8::String> func = frame->GetFunctionName();
ccstd::string funcName;
if (!func.IsEmpty()) {
funcName = *v8::String::Utf8Value(v8::Isolate::GetCurrent(), func);
}
stackStr += " - [";
snprintf(tmp, sizeof(tmp), "%d", i);
stackStr += tmp;
stackStr += "]";
stackStr += (funcName.empty() ? "anonymous" : funcName.c_str());
stackStr += "@";
stackStr += (scriptName.empty() ? "(no filename)" : scriptName.c_str());
stackStr += ":";
snprintf(tmp, sizeof(tmp), "%d", frame->GetLineNumber());
stackStr += tmp;
if (i < (e - 1)) {
stackStr += "\n";
}
}
return stackStr;
}
se::Value oldConsoleLog;
se::Value oldConsoleDebug;
se::Value oldConsoleInfo;
se::Value oldConsoleWarn;
se::Value oldConsoleError;
se::Value oldConsoleAssert;
bool jsbConsoleFormatLog(State &state, cc::LogLevel level, int msgIndex = 0) {
if (msgIndex < 0) {
return false;
}
const auto &args = state.args();
int argc = static_cast<int>(args.size());
if ((argc - msgIndex) == 1) {
ccstd::string msg = args[msgIndex].toStringForce();
cc::Log::logMessage(cc::LogType::KERNEL, level
,"JS: %s", msg.c_str());
} else if (argc > 1) {
ccstd::string msg = args[msgIndex].toStringForce();
size_t pos;
for (int i = (msgIndex + 1); i < argc; ++i) {
pos = msg.find('%');
if (pos != ccstd::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();
}
}
cc::Log::logMessage(cc::LogType::KERNEL, level
,"JS: %s", msg.c_str());
}
return true;
}
bool jsbConsoleLog(State &s) {
jsbConsoleFormatLog(s, cc::LogLevel::LEVEL_DEBUG);
oldConsoleLog.toObject()->call(s.args(), s.thisObject());
return true;
}
SE_BIND_FUNC(jsbConsoleLog)
bool jsbConsoleDebug(State &s) {
jsbConsoleFormatLog(s, cc::LogLevel::LEVEL_DEBUG);
oldConsoleDebug.toObject()->call(s.args(), s.thisObject());
return true;
}
SE_BIND_FUNC(jsbConsoleDebug)
bool jsbConsoleInfo(State &s) {
jsbConsoleFormatLog(s, cc::LogLevel::INFO);
oldConsoleInfo.toObject()->call(s.args(), s.thisObject());
return true;
}
SE_BIND_FUNC(jsbConsoleInfo)
bool jsbConsoleWarn(State &s) {
jsbConsoleFormatLog(s, cc::LogLevel::WARN);
oldConsoleWarn.toObject()->call(s.args(), s.thisObject());
return true;
}
SE_BIND_FUNC(jsbConsoleWarn)
bool jsbConsoleError(State &s) {
jsbConsoleFormatLog(s, cc::LogLevel::ERR);
oldConsoleError.toObject()->call(s.args(), s.thisObject());
return true;
}
SE_BIND_FUNC(jsbConsoleError)
bool jsbConsoleAssert(State &s) {
const auto &args = s.args();
if (!args.empty()) {
if (args[0].isBoolean() && !args[0].toBoolean()) {
jsbConsoleFormatLog(s, cc::LogLevel::WARN, 1);
oldConsoleAssert.toObject()->call(s.args(), s.thisObject());
}
}
return true;
}
SE_BIND_FUNC(jsbConsoleAssert)
/*
* The unique V8 platform instance
*/
#if !CC_EDITOR
class ScriptEngineV8Context {
public:
ScriptEngineV8Context() {
platform = v8::platform::NewDefaultPlatform().release();
v8::V8::InitializePlatform(platform);
ccstd::string flags;
// NOTICE: spaces are required between flags
flags.append(" --expose-gc-as=" EXPOSE_GC);
// for bytecode support
flags.append(" --no-flush-bytecode --no-lazy");
// v8 trace gc
// flags.append(" --trace-gc");
// NOTICE: should be remove flag --no-turbo-escape after upgrade v8 to 10.x
// https://github.com/cocos/cocos-engine/issues/13342
flags.append(" --no-turbo-escape");
#if (CC_PLATFORM == CC_PLATFORM_IOS)
flags.append(" --jitless");
#endif
if (!flags.empty()) {
v8::V8::SetFlagsFromString(flags.c_str(), static_cast<int>(flags.length()));
}
bool ok = v8::V8::Initialize();
CC_ASSERT(ok);
}
~ScriptEngineV8Context() {
v8::V8::Dispose();
#if V8_MAJOR_VERSION > 9 || ( V8_MAJOR_VERSION == 9 && V8_MINOR_VERSION > 7)
v8::V8::DisposePlatform();
#else
v8::V8::ShutdownPlatform();
#endif
delete platform;
}
v8::Platform *platform = nullptr;
};
ScriptEngineV8Context *gSharedV8 = nullptr;
#endif // CC_EDITOR
} // namespace
ScriptEngine *ScriptEngine::instance = nullptr;
ScriptEngine::DebuggerInfo ScriptEngine::debuggerInfo;
void ScriptEngine::callExceptionCallback(const char *location, const char *message, const char *stack) {
if (_nativeExceptionCallback) {
_nativeExceptionCallback(location, message, stack);
}
if (_jsExceptionCallback) {
_jsExceptionCallback(location, message, stack);
}
}
void ScriptEngine::onFatalErrorCallback(const char *location, const char *message) {
ccstd::string errorStr = "[FATAL ERROR] location: ";
errorStr += location;
errorStr += ", message: ";
errorStr += message;
SE_LOGE("%s\n", errorStr.c_str());
getInstance()->callExceptionCallback(location, message, "(no stack information)");
}
void ScriptEngine::onOOMErrorCallback(const char *location,
#if V8_MAJOR_VERSION > 10 || (V8_MAJOR_VERSION == 10 && V8_MINOR_VERSION > 4)
const v8::OOMDetails& details
#else
bool isHeapOom
#endif
) {
ccstd::string errorStr = "[OOM ERROR] location: ";
errorStr += location;
ccstd::string message;
message = "is heap out of memory: ";
#if V8_MAJOR_VERSION > 10 || (V8_MAJOR_VERSION == 10 && V8_MINOR_VERSION > 4)
if (details.is_heap_oom) {
#else
if (isHeapOom) {
#endif
message += "true";
} else {
message += "false";
}
errorStr += ", " + message;
SE_LOGE("%s\n", errorStr.c_str());
getInstance()->callExceptionCallback(location, message.c_str(), "(no stack information)");
}
void ScriptEngine::onMessageCallback(v8::Local<v8::Message> message, v8::Local<v8::Value> /*data*/) {
ScriptEngine *thiz = getInstance();
v8::Local<v8::String> msg = message->Get();
Value msgVal;
internal::jsToSeValue(v8::Isolate::GetCurrent(), msg, &msgVal);
CC_ASSERT(msgVal.isString());
v8::ScriptOrigin origin = message->GetScriptOrigin();
Value resouceNameVal;
internal::jsToSeValue(v8::Isolate::GetCurrent(), origin.ResourceName(), &resouceNameVal);
Value line(origin.LineOffset());
Value column(origin.ColumnOffset());
ccstd::string location = resouceNameVal.toStringForce() + ":" + line.toStringForce() + ":" + column.toStringForce();
ccstd::string errorStr = msgVal.toString() + ", location: " + location;
ccstd::string stackStr = stackTraceToString(message->GetStackTrace());
if (!stackStr.empty()) {
if (line.toInt32() == 0) {
location = "(see stack)";
}
errorStr += "\nSTACK:\n" + stackStr;
}
SE_LOGE("ERROR: %s\n", errorStr.c_str());
thiz->callExceptionCallback(location.c_str(), msgVal.toString().c_str(), stackStr.c_str());
if (!thiz->_isErrorHandleWorking) {
thiz->_isErrorHandleWorking = true;
Value errorHandler;
if (thiz->_globalObj && thiz->_globalObj->getProperty("__errorHandler", &errorHandler) && errorHandler.isObject() && errorHandler.toObject()->isFunction()) {
ValueArray args;
args.push_back(resouceNameVal);
args.push_back(line);
args.push_back(msgVal);
args.push_back(Value(stackStr));
errorHandler.toObject()->call(args, thiz->_globalObj);
}
thiz->_isErrorHandleWorking = false;
} else {
SE_LOGE("ERROR: __errorHandler has exception\n");
}
}
/**
* Bug in v8 stacktrace:
* "handlerAddedAfterPromiseRejected" event is triggered if a resolve handler is added.
* But if no reject handler is added, then "unhandledRejectedPromise" exception will be called again, but the stacktrace this time become empty
* LastStackTrace is used to store it.
*/
void ScriptEngine::pushPromiseExeception(const v8::Local<v8::Promise> &promise, const char *event, const char *stackTrace) {
using element_type = decltype(_promiseArray)::value_type;
element_type *current;
auto itr = std::find_if(_promiseArray.begin(), _promiseArray.end(), [&](const auto &e) -> bool {
return std::get<0>(e)->Get(_isolate) == promise;
});
if (itr == _promiseArray.end()) { // Not found, create one
_promiseArray.emplace_back(std::make_unique<v8::Persistent<v8::Promise>>(), ccstd::vector<PromiseExceptionMsg>{});
std::get<0>(_promiseArray.back())->Reset(_isolate, promise);
current = &_promiseArray.back();
} else {
current = &(*itr);
}
auto &exceptions = std::get<1>(*current);
if (strcmp(event, "handlerAddedAfterPromiseRejected") == 0) {
for (int i = 0; i < exceptions.size(); i++) {
if (exceptions[i].event == "unhandledRejectedPromise") {
_lastStackTrace = exceptions[i].stackTrace;
exceptions.erase(exceptions.begin() + i);
return;
}
}
}
exceptions.push_back(PromiseExceptionMsg{event, stackTrace});
}
void ScriptEngine::handlePromiseExceptions() {
if (_promiseArray.empty()) {
return;
}
for (auto &exceptionsPair : _promiseArray) {
auto &exceptionVector = std::get<1>(exceptionsPair);
for (const auto &exceptions : exceptionVector) {
getInstance()->callExceptionCallback("", exceptions.event.c_str(), exceptions.stackTrace.c_str());
}
std::get<0>(exceptionsPair).get()->Reset();
}
_promiseArray.clear();
_lastStackTrace.clear();
}
void ScriptEngine::onPromiseRejectCallback(v8::PromiseRejectMessage msg) {
/* Reject message contains different types, yet not every type will lead to the exception in the end.
* A detection is needed: if the reject handler is added after the promise is triggered, it's actually valid.*/
v8::Isolate *isolate = getInstance()->_isolate;
v8::HandleScope scope(isolate);
v8::TryCatch tryCatch(isolate);
std::stringstream ss;
auto event = msg.GetEvent();
v8::Local<v8::Value> value = msg.GetValue();
auto promiseName = msg.GetPromise()->GetConstructorName();
if (!value.IsEmpty()) {
// prepend error object to stack message
// v8::MaybeLocal<v8::String> maybeStr = value->ToString(isolate->GetCurrentContext());
if (value->IsString()) {
v8::Local<v8::String> str = value->ToString(isolate->GetCurrentContext()).ToLocalChecked();
v8::String::Utf8Value valueUtf8(isolate, str);
auto *strp = *valueUtf8;
if (strp == nullptr) {
ss << "value: null" << std::endl;
auto tn = value->TypeOf(isolate);
v8::String::Utf8Value tnUtf8(isolate, tn);
strp = *tnUtf8;
ss << " type: " << strp << std::endl;
}
} else if (value->IsObject()) {
v8::MaybeLocal<v8::String> json = v8::JSON::Stringify(isolate->GetCurrentContext(), value);
if (!json.IsEmpty()) {
v8::String::Utf8Value jsonStr(isolate, json.ToLocalChecked());
auto *strp = *jsonStr;
if (strp) {
ss << " obj: " << strp << std::endl;
} else {
ss << " obj: null" << std::endl;
}
} else {
v8::Local<v8::Object> obj = value->ToObject(isolate->GetCurrentContext()).ToLocalChecked();
v8::Local<v8::Array> attrNames = obj->GetOwnPropertyNames(isolate->GetCurrentContext()).ToLocalChecked();
if (!attrNames.IsEmpty()) {
uint32_t size = attrNames->Length();
for (uint32_t i = 0; i < size; i++) {
se::Value e;
v8::Local<v8::String> attrName = attrNames->Get(isolate->GetCurrentContext(), i)
.ToLocalChecked()
->ToString(isolate->GetCurrentContext())
.ToLocalChecked();
v8::String::Utf8Value attrUtf8(isolate, attrName);
auto *strp = *attrUtf8;
ss << " obj.property " << strp << std::endl;
}
ss << " obj: JSON.parse failed!" << std::endl;
}
}
}
v8::String::Utf8Value valuePromiseConstructor(isolate, promiseName);
auto *strp = *valuePromiseConstructor;
if (strp) {
ss << "PromiseConstructor " << strp;
}
}
auto stackStr = getInstance()->getCurrentStackTrace();
ss << "stacktrace: " << std::endl;
if (stackStr.empty()) {
ss << getInstance()->_lastStackTrace << std::endl;
} else {
ss << stackStr << std::endl;
}
// Check event immediately, for certain case throw exception.
switch (event) {
case v8::kPromiseRejectWithNoHandler:
getInstance()->pushPromiseExeception(msg.GetPromise(), "unhandledRejectedPromise", ss.str().c_str());
break;
case v8::kPromiseHandlerAddedAfterReject:
getInstance()->pushPromiseExeception(msg.GetPromise(), "handlerAddedAfterPromiseRejected", ss.str().c_str());
break;
case v8::kPromiseRejectAfterResolved:
getInstance()->callExceptionCallback("", "rejectAfterPromiseResolved", stackStr.c_str());
break;
case v8::kPromiseResolveAfterResolved:
getInstance()->callExceptionCallback("", "resolveAfterPromiseResolved", stackStr.c_str());
break;
}
}
ScriptEngine *ScriptEngine::getInstance() {
return ScriptEngine::instance;
}
void ScriptEngine::destroyInstance() {
}
ScriptEngine::ScriptEngine()
: _isolate(nullptr),
_handleScope(nullptr),
_globalObj(nullptr)
#if SE_ENABLE_INSPECTOR
,
_env(nullptr),
_isolateData(nullptr)
#endif
,
_debuggerServerPort(0),
_vmId(0),
_isValid(false),
_isGarbageCollecting(false),
_isInCleanup(false),
_isErrorHandleWorking(false) {
#if !CC_EDITOR
if (!gSharedV8) {
gSharedV8 = ccnew ScriptEngineV8Context();
}
#endif
ScriptEngine::instance = this;
}
ScriptEngine::~ScriptEngine() { // NOLINT(bugprone-exception-escape)
cleanup();
/**
* v8::V8::Initialize() can only be called once for a process.
* Engine::restart() will delete ScriptEngine and re-create it.
* So gSharedV8 variable should not be released and it will be re-used.
*/
// if (gSharedV8) {
// delete gSharedV8;
// gSharedV8 = nullptr;
// }
ScriptEngine::instance = nullptr;
}
bool ScriptEngine::postInit() {
v8::HandleScope hs(_isolate);
// editor has it's own isolate,no need to enter and set callback.
#if !CC_EDITOR
_isolate->Enter();
_isolate->SetCaptureStackTraceForUncaughtExceptions(true, JSB_STACK_FRAME_LIMIT, v8::StackTrace::kOverview);
_isolate->SetFatalErrorHandler(onFatalErrorCallback);
_isolate->SetOOMErrorHandler(onOOMErrorCallback);
_isolate->AddMessageListener(onMessageCallback);
_isolate->SetPromiseRejectCallback(onPromiseRejectCallback);
#endif
NativePtrToObjectMap::init();
Class::setIsolate(_isolate);
Object::setIsolate(_isolate);
_globalObj = Object::_createJSObject(nullptr, _isolate->GetCurrentContext()->Global());
_globalObj->root();
_globalObj->setProperty("window", Value(_globalObj));
#if !CC_EDITOR
se::Value consoleVal;
if (_globalObj->getProperty("console", &consoleVal) && consoleVal.isObject()) {
consoleVal.toObject()->getProperty("log", &oldConsoleLog);
consoleVal.toObject()->defineFunction("log", _SE(jsbConsoleLog));
consoleVal.toObject()->getProperty("debug", &oldConsoleDebug);
consoleVal.toObject()->defineFunction("debug", _SE(jsbConsoleDebug));
consoleVal.toObject()->getProperty("info", &oldConsoleInfo);
consoleVal.toObject()->defineFunction("info", _SE(jsbConsoleInfo));
consoleVal.toObject()->getProperty("warn", &oldConsoleWarn);
consoleVal.toObject()->defineFunction("warn", _SE(jsbConsoleWarn));
consoleVal.toObject()->getProperty("error", &oldConsoleError);
consoleVal.toObject()->defineFunction("error", _SE(jsbConsoleError));
consoleVal.toObject()->getProperty("assert", &oldConsoleAssert);
consoleVal.toObject()->defineFunction("assert", _SE(jsbConsoleAssert));
}
#endif
_globalObj->setProperty("scriptEngineType", se::Value("V8"));
_globalObj->defineFunction("log", seLogCallback);
_globalObj->defineFunction("forceGC", seForceGC);
_globalObj->getProperty(EXPOSE_GC, &_gcFuncValue);
if (_gcFuncValue.isObject() && _gcFuncValue.toObject()->isFunction()) {
_gcFunc = _gcFuncValue.toObject();
} else {
_gcFunc = nullptr;
}
_isValid = true;
// @deprecated since 3.7.0
cc::plugin::send(cc::plugin::BusType::SCRIPT_ENGINE, cc::plugin::ScriptEngineEvent::POST_INIT);
cc::events::ScriptEngine::broadcast(cc::ScriptEngineEvent::AFTER_INIT);
for (const auto &hook : _afterInitHookArray) {
hook();
}
_afterInitHookArray.clear();
return _isValid;
}
bool ScriptEngine::init() {
return init(nullptr);
}
bool ScriptEngine::init(v8::Isolate *isolate) {
cleanup();
SE_LOGD("Initializing V8, version: %s\n", v8::V8::GetVersion());
++_vmId;
_engineThreadId = std::this_thread::get_id();
cc::events::ScriptEngine::broadcast(cc::ScriptEngineEvent::BEFORE_INIT);
for (const auto &hook : _beforeInitHookArray) {
hook();
}
_beforeInitHookArray.clear();
if (isolate != nullptr) {
_isolate = isolate;
v8::Local<v8::Context> context = _isolate->GetCurrentContext();
_context.Reset(_isolate, context);
} else {
static v8::ArrayBuffer::Allocator *arrayBufferAllocator{nullptr};
if (arrayBufferAllocator == nullptr) {
arrayBufferAllocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator();
}
v8::Isolate::CreateParams createParams;
createParams.array_buffer_allocator = arrayBufferAllocator;
_isolate = v8::Isolate::New(createParams);
v8::HandleScope hs(_isolate);
_context.Reset(_isolate, v8::Context::New(_isolate));
_context.Get(_isolate)->Enter();
}
return postInit();
}
void ScriptEngine::cleanup() {
if (!_isValid) {
return;
}
SE_LOGD("ScriptEngine::cleanup begin ...\n");
_isInCleanup = true;
cc::events::ScriptEngine::broadcast(cc::ScriptEngineEvent::BEFORE_CLEANUP);
{
AutoHandleScope hs;
for (const auto &hook : _beforeCleanupHookArray) {
hook();
}
_beforeCleanupHookArray.clear();
_stringPool.clear();
SAFE_DEC_REF(_globalObj);
Object::cleanup();
Class::cleanup();
garbageCollect();
oldConsoleLog.setUndefined();
oldConsoleDebug.setUndefined();
oldConsoleInfo.setUndefined();
oldConsoleWarn.setUndefined();
oldConsoleError.setUndefined();
oldConsoleAssert.setUndefined();
#if SE_ENABLE_INSPECTOR
if (_env != nullptr) {
_env->inspector_agent()->Disconnect();
_env->inspector_agent()->Stop();
}
if (_isolateData != nullptr) {
node::FreeIsolateData(_isolateData);
_isolateData = nullptr;
}
if (_env != nullptr) {
_env->CleanupHandles();
node::FreeEnvironment(_env);
_env = nullptr;
}
#endif
_context.Get(_isolate)->Exit();
_context.Reset();
_isolate->Exit();
}
_isolate->Dispose();
_isolate = nullptr;
Object::setIsolate(nullptr);
_globalObj = nullptr;
_isValid = false;
_registerCallbackArray.clear();
for (const auto &hook : _afterCleanupHookArray) {
hook();
}
// Cleanup all hooks
_beforeInitHookArray.clear();
_afterInitHookArray.clear();
_beforeCleanupHookArray.clear();
_afterCleanupHookArray.clear();
_isInCleanup = false;
NativePtrToObjectMap::destroy();
_gcFuncValue.setUndefined();
_gcFunc = nullptr;
cc::events::ScriptEngine::broadcast(cc::ScriptEngineEvent::AFTER_CLEANUP);
SE_LOGD("ScriptEngine::cleanup end ...\n");
}
Object *ScriptEngine::getGlobalObject() const {
return _globalObj;
}
void ScriptEngine::addBeforeInitHook(const std::function<void()> &hook) {
_beforeInitHookArray.push_back(hook);
}
void ScriptEngine::addAfterInitHook(const std::function<void()> &hook) {
_afterInitHookArray.push_back(hook);
}
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::addRegisterCallback(RegisterCallback cb) {
CC_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);
}
}
bool ScriptEngine::callRegisteredCallback() {
se::AutoHandleScope hs;
bool ok = false;
_startTime = std::chrono::steady_clock::now();
for (auto cb : _permRegisterCallbackArray) {
ok = cb(_globalObj);
CC_ASSERT(ok);
if (!ok) {
break;
}
}
for (auto cb : _registerCallbackArray) {
ok = cb(_globalObj);
CC_ASSERT(ok);
if (!ok) {
break;
}
}
// After ScriptEngine is started, _registerCallbackArray isn't needed. Therefore, clear it here.
_registerCallbackArray.clear();
return ok;
}
bool ScriptEngine::start() {
if (!init()) {
return false;
}
se::AutoHandleScope hs;
// Check the cache of debuggerInfo. Enable debugger if it's valid.
if (debuggerInfo.isValid()) {
enableDebugger(debuggerInfo.serverAddr, debuggerInfo.port, debuggerInfo.isWait);
debuggerInfo.reset();
}
// debugger
if (isDebuggerEnabled()) {
#if SE_ENABLE_INSPECTOR && !CC_EDITOR
// V8 inspector stuff, most code are taken from NodeJS.
_isolateData = node::CreateIsolateData(_isolate, uv_default_loop());
_env = node::CreateEnvironment(_isolateData, _context.Get(_isolate), 0, nullptr, 0, nullptr);
node::DebugOptions options;
options.set_wait_for_connect(_isWaitForConnect); // the program will be hung up until debug attach if _isWaitForConnect = true
options.set_inspector_enabled(true);
options.set_port(static_cast<int>(_debuggerServerPort));
options.set_host_name(_debuggerServerAddr);
bool ok = _env->inspector_agent()->Start(gSharedV8->platform, "", options);
CC_ASSERT(ok);
#endif
}
return callRegisteredCallback();
}
bool ScriptEngine::start(v8::Isolate *isolate) {
if (!init(isolate)) {
return false;
}
return callRegisteredCallback();
}
void ScriptEngine::garbageCollect() {
SE_LOGD("GC begin ..., (js->native map) size: %d\n", (int)NativePtrToObjectMap::size());
_gcFunc->call({}, nullptr);
SE_LOGD("GC end ..., (js->native map) size: %d\n", (int)NativePtrToObjectMap::size());
}
bool ScriptEngine::isGarbageCollecting() const {
return _isGarbageCollecting;
}
void ScriptEngine::_setGarbageCollecting(bool isGarbageCollecting) { // NOLINT(readability-identifier-naming)
_isGarbageCollecting = isGarbageCollecting;
}
/* static */
void ScriptEngine::_setDebuggerInfo(const DebuggerInfo &info) {
debuggerInfo = info;
}
bool ScriptEngine::isValid() const {
return ScriptEngine::instance != nullptr && _isValid;
}
bool ScriptEngine::evalString(const char *script, uint32_t length /* = 0 */, Value *ret /* = nullptr */, const char *fileName /* = nullptr */) {
if (_engineThreadId != std::this_thread::get_id()) {
// `evalString` should run in main thread
CC_ABORT();
return false;
}
CC_ASSERT_NOT_NULL(script);
if (length == 0) {
length = static_cast<uint32_t>(strlen(script));
}
if (fileName == nullptr) {
fileName = "(no filename)";
}
// Fix the source url is too long displayed in Chrome debugger.
ccstd::string sourceUrl = fileName;
static const ccstd::string PREFIX_KEY = "/temp/quick-scripts/";
size_t prefixPos = sourceUrl.find(PREFIX_KEY);
if (prefixPos != ccstd::string::npos) {
sourceUrl = sourceUrl.substr(prefixPos + PREFIX_KEY.length());
}
// It is needed, or will crash if invoked from non C++ context, such as invoked from objective-c context(for example, handler of UIKit).
v8::HandleScope handleScope(_isolate);
ccstd::string scriptStr(script, length);
v8::MaybeLocal<v8::String> source = v8::String::NewFromUtf8(_isolate, scriptStr.c_str(), v8::NewStringType::kNormal);
if (source.IsEmpty()) {
return false;
}
v8::MaybeLocal<v8::String> originStr = v8::String::NewFromUtf8(_isolate, sourceUrl.c_str(), v8::NewStringType::kNormal);
if (originStr.IsEmpty()) {
return false;
}
v8::ScriptOrigin origin(_isolate, originStr.ToLocalChecked());
v8::MaybeLocal<v8::Script> maybeScript = v8::Script::Compile(_context.Get(_isolate), source.ToLocalChecked(), &origin);
bool success = false;
if (!maybeScript.IsEmpty()) {
v8::TryCatch block(_isolate);
v8::Local<v8::Script> v8Script = maybeScript.ToLocalChecked();
v8::MaybeLocal<v8::Value> maybeResult = v8Script->Run(_context.Get(_isolate));
if (!maybeResult.IsEmpty()) {
v8::Local<v8::Value> result = maybeResult.ToLocalChecked();
if (!result->IsUndefined() && ret != nullptr) {
internal::jsToSeValue(_isolate, result, ret);
}
success = true;
}
if (block.HasCaught()) {
v8::Local<v8::Message> message = block.Message();
SE_LOGE("ScriptEngine::evalString catch exception:\n");
onMessageCallback(message, v8::Undefined(_isolate));
}
}
if (!success) {
SE_LOGE("ScriptEngine::evalString script %s, failed!\n", fileName);
}
return success;
}
ccstd::string ScriptEngine::getCurrentStackTrace() {
if (!_isValid) {
return ccstd::string();
}
v8::HandleScope hs(_isolate);
v8::Local<v8::StackTrace> stack = v8::StackTrace::CurrentStackTrace(_isolate, JSB_STACK_FRAME_LIMIT, v8::StackTrace::kOverview);
return stackTraceToString(stack);
}
void ScriptEngine::setFileOperationDelegate(const FileOperationDelegate &delegate) {
_fileOperationDelegate = delegate;
}
const ScriptEngine::FileOperationDelegate &ScriptEngine::getFileOperationDelegate() const {
return _fileOperationDelegate;
}
bool ScriptEngine::saveByteCodeToFile(const ccstd::string &path, const ccstd::string &pathBc) {
bool success = false;
auto *fu = cc::FileUtils::getInstance();
if (pathBc.length() > 3 && pathBc.substr(pathBc.length() - 3) != ".bc") {
SE_LOGE("ScriptEngine::generateByteCode bytecode file path should endwith \".bc\"\n");
;
return false;
}
if (fu->isFileExist(pathBc)) {
SE_LOGE("ScriptEngine::generateByteCode file already exists, it will be rewrite!\n");
}
// create directory for .bc file
{
auto lastSep = static_cast<int>(pathBc.size()) - 1;
while (lastSep >= 0 && pathBc[lastSep] != '/') {
lastSep -= 1;
}
if (lastSep == 0) {
SE_LOGE("ScriptEngine::generateByteCode no directory component found in path %s\n", path.c_str());
return false;
}
ccstd::string pathBcDir = pathBc.substr(0, lastSep);
success = fu->createDirectory(pathBcDir);
if (!success) {
SE_LOGE("ScriptEngine::generateByteCode failed to create bytecode for %s\n", path.c_str());
return success;
}
}
// load script file
ccstd::string scriptBuffer = _fileOperationDelegate.onGetStringFromFile(path);
v8::Local<v8::String> code = v8::String::NewFromUtf8(_isolate, scriptBuffer.c_str(), v8::NewStringType::kNormal, static_cast<int>(scriptBuffer.length())).ToLocalChecked();
v8::Local<v8::Value> scriptPath = v8::String::NewFromUtf8(_isolate, path.data(), v8::NewStringType::kNormal).ToLocalChecked();
// create unbound script
v8::ScriptOrigin origin(_isolate, scriptPath);
v8::ScriptCompiler::Source source(code, origin);
v8::Local<v8::Context> parsingContext = v8::Local<v8::Context>::New(_isolate, _context);
v8::Context::Scope parsingScope(parsingContext);
v8::TryCatch tryCatch(_isolate);
v8::Local<v8::UnboundScript> v8Script = v8::ScriptCompiler::CompileUnboundScript(_isolate, &source, v8::ScriptCompiler::kEagerCompile)
.ToLocalChecked();
// create CachedData
v8::ScriptCompiler::CachedData *cd = v8::ScriptCompiler::CreateCodeCache(v8Script);
if (cd != nullptr) {
// save to file
cc::Data writeData;
writeData.copy(cd->data, cd->length);
success = fu->writeDataToFile(writeData, pathBc);
if (!success) {
SE_LOGE("ScriptEngine::generateByteCode write %s\n", pathBc.c_str());
}
// TODO(PatriceJiang): v8 on windows is built with dynamic library (dll),
// Invoking `delete` for the memory allocated in v8.dll will cause crash.
// Need to modify v8 source code and add v8::ScriptCompiler::DestroyCodeCache(v8::ScriptCompiler::CachedData *cd).
#if CC_PLATFORM != CC_PLATFORM_WINDOWS
delete cd;
#endif
} else {
success = false;
}
return success;
}
bool ScriptEngine::runByteCodeFile(const ccstd::string &pathBc, Value *ret /* = nullptr */) {
auto *fu = cc::FileUtils::getInstance();
cc::Data cachedData;
fu->getContents(pathBc, &cachedData);
// read origin source file length from .bc file
uint8_t *p = cachedData.getBytes() + 8;
int filesize = p[0] + (p[1] << 8) + (p[2] << 16) + (p[3] << 24);
{
// fix bytecode
v8::HandleScope scope(_isolate);
v8::Local<v8::String> dummyBytecodeSource = v8::String::NewFromUtf8(_isolate, "\" \"", v8::NewStringType::kNormal).ToLocalChecked();
v8::ScriptCompiler::Source dummySource(dummyBytecodeSource);
v8::Local<v8::UnboundScript> dummyFunction = v8::ScriptCompiler::CompileUnboundScript(_isolate, &dummySource, v8::ScriptCompiler::kEagerCompile).ToLocalChecked();
v8::ScriptCompiler::CachedData *dummyData = v8::ScriptCompiler::CreateCodeCache(dummyFunction);
memcpy(p + 4, dummyData->data + 12, 4);
// TODO(PatriceJiang): v8 on windows is built with dynamic library (dll),
// Invoking `delete` for the memory allocated in v8.dll will cause crash.
// Need to modify v8 source code and add v8::ScriptCompiler::DestroyCodeCache(v8::ScriptCompiler::CachedData *cd).
#if CC_PLATFORM != CC_PLATFORM_WINDOWS
delete dummyData;
#endif
}
// setup ScriptOrigin
v8::Local<v8::Value> scriptPath = v8::String::NewFromUtf8(_isolate, pathBc.data(), v8::NewStringType::kNormal).ToLocalChecked();
v8::ScriptOrigin origin(_isolate, scriptPath, 0, 0, true);
// restore CacheData
auto *v8CacheData = ccnew v8::ScriptCompiler::CachedData(cachedData.getBytes(), static_cast<int>(cachedData.getSize()));
v8::Local<v8::String> dummyCode;
// generate dummy code
if (filesize > 0) {
ccstd::vector<char> codeBuffer;
codeBuffer.resize(filesize + 1);
std::fill(codeBuffer.begin(), codeBuffer.end(), ' ');
codeBuffer[0] = '\"';
codeBuffer[filesize - 1] = '\"';
codeBuffer[filesize] = '\0';
dummyCode = v8::String::NewFromUtf8(_isolate, codeBuffer.data(), v8::NewStringType::kNormal, filesize).ToLocalChecked();
CC_ASSERT(dummyCode->Length() == filesize);
}
v8::ScriptCompiler::Source source(dummyCode, origin, v8CacheData);
if (source.GetCachedData() == nullptr) {
SE_LOGE("ScriptEngine::runByteCodeFile can not load cacheData for %s", pathBc.c_str());
return false;
}
v8::TryCatch tryCatch(_isolate);
v8::Local<v8::UnboundScript> v8Script = v8::ScriptCompiler::CompileUnboundScript(_isolate, &source, v8::ScriptCompiler::kConsumeCodeCache)
.ToLocalChecked();
if (v8Script.IsEmpty()) {
SE_LOGE("ScriptEngine::runByteCodeFile can not compile %s!\n", pathBc.c_str());
return false;
}
if (source.GetCachedData()->rejected) {
SE_LOGE("ScriptEngine::runByteCodeFile cache rejected %s!\n", pathBc.c_str());
return false;
}
v8::Local<v8::Script> runnableScript = v8Script->BindToCurrentContext();
v8::MaybeLocal<v8::Value> result = runnableScript->Run(_context.Get(_isolate));
if (result.IsEmpty()) {
SE_LOGE("ScriptEngine::runByteCodeFile script %s, failed!\n", pathBc.c_str());
return false;
}
if (!result.ToLocalChecked()->IsUndefined() && ret != nullptr) {
internal::jsToSeValue(_isolate, result.ToLocalChecked(), ret);
}
SE_LOGE("ScriptEngine::runByteCodeFile success %s!\n", pathBc.c_str());
return true;
}
bool ScriptEngine::runScript(const ccstd::string &path, Value *ret /* = nullptr */) {
CC_ASSERT(!path.empty());
CC_ASSERT(_fileOperationDelegate.isValid());
if (!cc::FileUtils::getInstance()->isFileExist(path)) {
std::stringstream ss;
ss << "throw new Error(\"Failed to require file '"
<< path << "', not found!\");";
evalString(ss.str().c_str());
return false;
}
if (path.length() > 3 && path.substr(path.length() - 3) == ".bc") {
return runByteCodeFile(path, ret);
}
ccstd::string scriptBuffer = _fileOperationDelegate.onGetStringFromFile(path);
if (!scriptBuffer.empty()) {
return evalString(scriptBuffer.c_str(), static_cast<uint32_t>(scriptBuffer.length()), ret, path.c_str());
}
SE_LOGE("ScriptEngine::runScript script %s, buffer is empty!\n", path.c_str());
return false;
}
void ScriptEngine::clearException() {
// IDEA:
}
void ScriptEngine::throwException(const ccstd::string &errorMessage) {
v8::HandleScope scope(_isolate);
v8::Local<v8::String> message = v8::String::NewFromUtf8(_isolate, errorMessage.data()).ToLocalChecked();
v8::Local<v8::Value> error = v8::Exception::Error(message);
_isolate->ThrowException(error);
}
void ScriptEngine::setExceptionCallback(const ExceptionCallback &cb) {
_nativeExceptionCallback = cb;
}
void ScriptEngine::setJSExceptionCallback(const ExceptionCallback &cb) {
_jsExceptionCallback = cb;
}
v8::Local<v8::Context> ScriptEngine::_getContext() const { // NOLINT(readability-identifier-naming)
return _context.Get(_isolate);
}
void ScriptEngine::enableDebugger(const ccstd::string &serverAddr, uint32_t port, bool isWait) {
_debuggerServerAddr = serverAddr;
_debuggerServerPort = port;
_isWaitForConnect = isWait;
}
bool ScriptEngine::isDebuggerEnabled() const {
return !_debuggerServerAddr.empty() && _debuggerServerPort > 0;
}
void ScriptEngine::mainLoopUpdate() {
// empty implementation
}
bool ScriptEngine::callFunction(Object *targetObj, const char *funcName, uint32_t argc, Value *args, Value *rval /* = nullptr*/) {
v8::HandleScope handleScope(_isolate);
v8::MaybeLocal<v8::String> nameValue = _getStringPool().get(_isolate, funcName);
if (nameValue.IsEmpty()) {
if (rval != nullptr) {
rval->setUndefined();
}
return false;
}
v8::Local<v8::String> nameValToLocal = nameValue.ToLocalChecked();
v8::Local<v8::Context> context = _isolate->GetCurrentContext();
v8::Local<v8::Object> localObj = targetObj->_obj.handle(_isolate);
v8::MaybeLocal<v8::Value> funcVal = localObj->Get(context, nameValToLocal);
if (funcVal.IsEmpty()) {
if (rval != nullptr) {
rval->setUndefined();
}
return false;
}
SE_ASSERT(argc < 11, "Only support argument count that less than 11"); // NOLINT
ccstd::array<v8::Local<v8::Value>, 10> argv;
for (size_t i = 0; i < argc; ++i) {
internal::seToJsValue(_isolate, args[i], &argv[i]);
}
#if CC_DEBUG
v8::TryCatch tryCatch(_isolate);
#endif
CC_ASSERT(!funcVal.IsEmpty());
if (!funcVal.ToLocalChecked()->IsFunction()) {
v8::String::Utf8Value funcStr(_isolate, funcVal.ToLocalChecked());
SE_REPORT_ERROR("%s is not a function: %s", funcName, *funcStr);
return false;
}
v8::MaybeLocal<v8::Object> funcObj = funcVal.ToLocalChecked()->ToObject(context);
v8::MaybeLocal<v8::Value> result = funcObj.ToLocalChecked()->CallAsFunction(_getContext(), localObj, argc, argv.data());
#if CC_DEBUG
if (tryCatch.HasCaught()) {
v8::String::Utf8Value stack(_isolate, tryCatch.StackTrace(context).ToLocalChecked());
SE_REPORT_ERROR("Invoking function failed, %s", *stack);
if (rval != nullptr) {
rval->setUndefined();
}
tryCatch.ReThrow();
return false;
}
#endif
if (!result.IsEmpty()) {
if (rval != nullptr) {
internal::jsToSeValue(_isolate, result.ToLocalChecked(), rval);
}
}
return true;
}
// VMStringPool
ScriptEngine::VMStringPool::VMStringPool() = default;
ScriptEngine::VMStringPool::~VMStringPool() = default;
v8::MaybeLocal<v8::String> ScriptEngine::VMStringPool::get(v8::Isolate *isolate, const char *name) {
v8::Local<v8::String> ret;
auto iter = _vmStringPoolMap.find(name);
if (iter == _vmStringPoolMap.end()) {
v8::MaybeLocal<v8::String> nameValue = v8::String::NewFromUtf8(isolate, name, v8::NewStringType::kNormal);
if (!nameValue.IsEmpty()) {
auto *persistentName = ccnew v8::Persistent<v8::String>();
persistentName->Reset(isolate, nameValue.ToLocalChecked());
_vmStringPoolMap.emplace(name, persistentName);
ret = v8::Local<v8::String>::New(isolate, *persistentName);
}
} else {
ret = v8::Local<v8::String>::New(isolate, *iter->second);
}
return ret;
}
void ScriptEngine::VMStringPool::clear() {
for (auto &e : _vmStringPoolMap) {
e.second->Reset();
delete e.second;
}
_vmStringPoolMap.clear();
}
} // namespace se
#endif // #if SCRIPT_ENGINE_TYPE == SCRIPT_ENGINE_V8