ScriptEngine 类分析

Python3.8

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

ScriptEngine 类分析

1 概述

ScriptEngine 类是 Cocos Creator 引擎中用于管理 JavaScript 脚本执行的核心组件。它负责初始化脚本环境、执行脚本文件、处理异常、管理垃圾回收等关键功能。

ScriptEngine 类有多个实现版本,分别对应不同的 JavaScript 引擎

#define SCRIPT_ENGINE_NONE 0
#define SCRIPT_ENGINE_SM   1
#define SCRIPT_ENGINE_V8   2
#define SCRIPT_ENGINE_NAPI 5
#define SCRIPT_ENGINE_JSVM 6

#ifndef SCRIPT_ENGINE_TYPE
#if CC_PLATFORM == CC_PLATFORM_OPENHARMONY
    #define SCRIPT_ENGINE_TYPE SCRIPT_ENGINE_NAPI
#else
    #define SCRIPT_ENGINE_TYPE SCRIPT_ENGINE_V8
#endif
#endif

V8 版本: native/cocos/bindings/jswrapper/v8/ScriptEngine.cpp - 用于大多数平台
JSVM 版本: native/cocos/bindings/jswrapper/jsvm/ScriptEngine.cpp - 用于 OpenHarmony 平台
NAPI 版本: native/cocos/bindings/jswrapper/napi/ScriptEngine.cpp - 另一个 OpenHarmony 实现
SpiderMonkey 版本: native/cocos/bindings/jswrapper/sm/ScriptEngine.cpp - 备用引擎

config.h:32-44

2 ScriptEngine 类的实现分析(v8)

2.1 构造和析构函数分析

2.1.1 构造函数

ScriptEngine::ScriptEngine() {
    #if !CC_EDITOR
    if (!gSharedV8) {
        gSharedV8 = ccnew ScriptEngineV8Context();
    }
    #endif

    ScriptEngine::instance = this;
}

构造函数非常简单,只进行基本的初始化。它不执行任何复杂的初始化操作,将初始化工作推迟到 init() 方法中进行。

ScriptEngine.cpp:31:60

2.1.2 析构函数

ScriptEngine::~ScriptEngine() {
    cleanup();
    gSriptEngineInstance = nullptr;
};

析构函数是默认实现,不执行任何特殊操作。实际的清理工作在 cleanup() 方法中完成。

ScriptEngine.cpp:31-60

2.2 初始化和清理

2.2.1 init方法

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> const 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 const hs(_isolate);
        _context.Reset(_isolate, v8::Context::New(_isolate));
        _context.Get(_isolate)->Enter();
    }

    return postInit();
}

1)初始化准备阶段

方法首先调用 cleanup() 清理之前的状态,然后递增 _vmId 计数器,记录当前线程 ID ScriptEngine.cpp:434-438 。

接着触发 BEFORE_INIT 事件并执行所有注册的 beforeInit 钩子函数 ScriptEngine.cpp:440-445 。

2) Isolate 和 Context 创建

方法根据是否传入外部 isolate 参数采取不同策略 ScriptEngine.cpp:447-462 :

情况 A: 使用外部 Isolate (Editor 模式)

  • 直接使用传入的 isolate 指针
  • 获取当前 Context 并保存到 _context 成员变量

情况 B: 创建新 Isolate (运行时模式)

  • 创建 v8::ArrayBuffer::Allocator (仅首次创建,后续复用)
  • 配置 v8::Isolate::CreateParams 并创建新的 Isolate
  • 创建新的 V8 Context 并进入该 Context

3)postInit 后续初始化

最后调用 postInit() 完成剩余初始化工作 ScriptEngine.cpp:464 。

2.2.2 postInit 方法详解

bool ScriptEngine::postInit() {
    v8::HandleScope const 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(ScriptEngine::onFatalErrorCallback);
    _isolate->SetOOMErrorHandler(ScriptEngine::onOOMErrorCallback);
    _isolate->AddMessageListener(ScriptEngine::onMessageCallback);
    _isolate->SetPromiseRejectCallback(ScriptEngine::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;
}

postInit 方法执行以下关键步骤 ScriptEngine.cpp:360-427 :

1) 设置 V8 错误处理器

在非 Editor 模式下,配置 V8 的各种错误回调 ScriptEngine.cpp:363-370 :

  • SetCaptureStackTraceForUncaughtExceptions: 捕获未处理异常的堆栈信息
  • SetFatalErrorHandler: 致命错误处理器
  • SetOOMErrorHandler: 内存溢出错误处理器
  • AddMessageListener: 消息监听器
  • SetPromiseRejectCallback: Promise 拒绝回调

2)初始化对象映射系统

初始化 Native 指针到 JS 对象的映射表,并设置 ClassObject 的 Isolate ScriptEngine.cpp:371-374 。

3)创建全局对象

创建全局 JavaScript 对象并设置 window 属性指向自身 ScriptEngine.cpp:376-378 。

4) 重写 console 方法

在非 Editor 模式下,重写 console 对象的各个方法以支持自定义日志格式化 ScriptEngine.cpp:380-401 :

  • log, debug, info, warn, error, assert

这些方法会调用对应的 jsbConsoleLog, jsbConsoleDebug 等函数,它们使用 jsbConsoleFormatLog 进行格式化输出 ScriptEngine.cpp:208-220 。

5)注册全局函数

设置引擎类型标识和全局工具函数 ScriptEngine.cpp:402-412 :

  • scriptEngineType: 设置为 “V8”
  • log: 日志函数
  • forceGC: 强制垃圾回收函数
  • 获取 V8 暴露的 __jsb_gc__ 函数引用

6)触发初始化完成事件

标记引擎为有效状态,触发 AFTER_INIT 事件并执行所有 afterInit 钩子 ScriptEngine.cpp:414-426 。

ScriptEngine.cpp:91-120

2.2.3 cleanup 方法

void ScriptEngine::cleanup() {
    if (!_isValid) {
        return;
    }

    SE_LOGD("ScriptEngine::cleanup begin ...\n");
    _isInCleanup = true;

    cc::events::ScriptEngine::broadcast(cc::ScriptEngineEvent::BEFORE_CLEANUP);

    {
        AutoHandleScope const hs;
        for (const auto &hook : _beforeCleanupHookArray) {
            hook();
        }
        _beforeCleanupHookArray.clear();
        
        // Clear unhandled promises
        _unhandledPromises.clear();
        _ignoreUnhandledPromises = false;

        _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");
}

1) 有效性检查与状态设置

方法首先检查 _isValid 标志,如果引擎已经无效则直接返回 ScriptEngine.cpp:468-470 。然后设置 _isInCleanup 标志为 true,这个标志用于防止在清理过程中执行某些操作 ScriptEngine.cpp:472-473 。

2)触发清理前事件

触发 BEFORE_CLEANUP 事件并执行所有注册的 beforeCleanup 钩子函数 ScriptEngine.cpp:475-482 。这些钩子允许外部代码在清理开始前执行必要的清理工作,例如在 jsb_module_register.cpp 中注册的钩子会清理 DeferredReleasePoolJSBClassType jsb_module_register.cpp:265-268 。

3)核心资源清理

AutoHandleScope 作用域内执行核心清理操作 ScriptEngine.cpp:478-523 :

清理 Promise 相关状态

清空未处理的 Promise 队列并重置忽略标志 ScriptEngine.cpp:484-486 。这些 Promise 在 onPromiseRejectCallbackPerInstance 中被跟踪 ScriptEngine.cpp:1221-1248 。

清理字符串池

清空 _stringPool,释放缓存的字符串资源 ScriptEngine.cpp:488 。

清理全局对象和映射系统

按顺序清理核心组件 ScriptEngine.cpp:490-493 :

  • 释放全局对象 _globalObj 的引用
  • 调用 Object::cleanup() 清理所有 JS 对象包装器
  • 调用 Class::cleanup() 清理所有注册的类
  • 执行垃圾回收 garbageCollect() ScriptEngine.cpp:649-653
清理 console 对象引用

重置所有保存的 console 方法引用 ScriptEngine.cpp:495-500 。这些引用在 postInit 中被保存 ScriptEngine.cpp:383-399 。

清理调试器资源 (如果启用)

如果启用了调试器支持 (SE_ENABLE_INSPECTOR),则清理调试器相关资源 ScriptEngine.cpp:502-519 :

  • 断开并停止 inspector agent
  • 释放 isolate data
  • 清理并释放 environment
退出 V8 Context 和 Isolate

退出当前 Context 并重置,然后退出 Isolate ScriptEngine.cpp:521-523 。

4)释放 Isolate 和重置状态

AutoHandleScope 作用域外,释放 Isolate 并重置相关状态 ScriptEngine.cpp:525-532 :

  • 调用 _isolate->Dispose() 释放 Isolate
  • _isolate 设为 nullptr
  • 重置 Object 的 Isolate 引用
  • 清空 _globalObj 指针
  • 设置 _isValidfalse
  • 清空注册回调数组

5)触发清理后事件和最终清理

执行 afterCleanup 钩子并清理所有钩子数组 ScriptEngine.cpp:534-542 。重置 _isInCleanup 标志,销毁 Native 指针映射表,清理 GC 函数引用,最后触发 AFTER_CLEANUP 事件 ScriptEngine.cpp:544-549 。

2.2.4 脚本执行方法

runScript - 执行脚本文件
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 const 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;
}

该方法从文件系统读取脚本并执行: ScriptEngine.cpp:919-943

evalString - 执行字符串代码
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)";
    }

    v8::TryCatch const tryCatch(_isolate);
    // 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 const 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 const handleScope(_isolate);

    ccstd::string const 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()) {
        reportException(_isolate, tryCatch);
    } else {
        v8::Local<v8::Script> const v8Script = maybeScript.ToLocalChecked();
        v8::MaybeLocal<v8::Value> maybeResult = v8Script->Run(_context.Get(_isolate));

        if (!maybeResult.IsEmpty()) {
            v8::Local<v8::Value> const result = maybeResult.ToLocalChecked();

            if (!result->IsUndefined() && ret != nullptr) {
                internal::jsToSeValue(_isolate, result, ret);
            }

            success = true;
        }

        if (tryCatch.HasCaught()) {
            SE_LOGE("ScriptEngine::evalString catch exception:\n");
            reportException(_isolate, tryCatch);
        }
    }

    if (!success) {
        SE_LOGE("ScriptEngine::evalString script %s, failed!\n", fileName);
    }

    return success;
}

直接执行 JavaScript 代码字符串: ScriptEngine.cpp:672-743。

2.2.5 字节码相关方法

saveByteCodeToFile - 保存字节码

V8 版本支持将脚本编译为字节码并保存: ScriptEngine.cpp:763-831

关键步骤:

  1. 验证输出路径以 .bc 结尾
  2. 创建目录结构
  3. 加载并编译脚本为 UnboundScript
  4. 生成 CachedData 并写入文件
runByteCodeFile - 执行字节码

.bc 文件加载并执行预编译的字节码: ScriptEngine.cpp:833-917

2.2.6 调试与诊断方法

getCurrentStackTrace - 获取调用栈

生成当前 JavaScript 执行栈的字符串表示: ScriptEngine.cpp:745-753

栈信息格式化由 stackTraceToString 辅助函数完成: ScriptEngine.cpp:90-139

enableDebugger - 启用调试器

配置调试器连接参数: ScriptEngine.cpp:452-461

2.2.7 辅助工具类

AutoHandleScope - 自动句柄作用域管理

V8 版本使用 RAII 模式管理 V8 的 HandleScope: ScriptEngine.cpp:64-75

JSVM 版本类似: ScriptEngine.cpp:43-49

2.2.8 文件操作委托

FileOperationDelegate 提供文件系统抽象接口: ScriptEngine.h:54-77

通过 setFileOperationDelegategetFileOperationDelegate 方法设置和获取: ScriptEngine.cpp:155-161

2.2.9 事件处理

Promise 拒绝回调

V8 版本处理未捕获的 Promise 拒绝

2.2.10 日志回调

提供 JavaScript 日志输出到 C++ 的桥接

3.总结

ScriptEngine 类是 Cocos Creator 引擎中用于管理 JavaScript 脚本执行的核心组件。它负责初始化脚本环境、执行脚本文件、处理异常、管理垃圾回收等关键功能。主要特点包括:

  1. 提供初始化和清理方法,管理脚本引擎的生命周期
  2. 支持执行脚本文件和字符串形式的脚本代码
  3. 提供钩子函数,允许在初始化和清理过程中执行自定义逻辑
  4. 提供全局对象访问和方法调用功能

该类的设计体现了模块化和可扩展性的原则,通过钩子函数机制允许外部组件在关键阶段插入自定义逻辑。虽然部分功能如异常处理和垃圾回收尚未完全实现,但整体架构为脚本执行提供了坚实的基础。

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值