Files
BlueRoseNote/07-Other/AI/AI Agent/UnrealEngine/Puerts/Puerts 架构深度分析.md

15 KiB
Raw Blame History

tags, created
tags created
UE
Puerts
TypeScript
V8
架构分析
2026-05-31

Puerts 架构深度分析

概述

Puerts发音 "pu-erts")是腾讯开源的 Unreal Engine TypeScript/JavaScript 运行时。它将 V8 JavaScript 引擎直接嵌入 UE 的模块系统,实现 C++ 和 TypeScript 的双向互操作。

  • 仓库: D:\MatrixTA\puerts
  • UE 端源码: puerts\unreal\Puerts\
  • 许可证: BSD 3-Clause

插件结构

Puerts.uplugin 定义了 6 个模块,有严格的初始化顺序:

模块 类型 LoadingPhase 用途
WasmCore Runtime PreDefault WebAssembly (wasm3) 支持
JsEnv Runtime PreDefault 核心 V8 嵌入引擎
ParamDefaultValueMetas Program PostConfigInit UHT 插件,参数默认值
Puerts Runtime PostEngineInit 高层模块设置、JsEnv 生命周期、PIE 钩子
DeclarationGenerator Editor Default 从 UE 反射生成 *.d.ts
PuertsEditor Editor PostEngineInit 编辑器集成、TS 编译监听

依赖链

WasmCore → Core, CoreUObject, Engine
JsEnv → Core, CoreUObject, Engine, UMG, ParamDefaultValueMetas, WasmCore, Json
Puerts → Core, CoreUObject, Engine, Serialization, OpenSSL, UMG, JsEnv
PuertsEditor → Puerts, JsEnv, UnrealEd, LevelEditor, DirectoryWatcher, AssetRegistry
DeclarationGenerator → Puerts, JsEnv, UnrealEd

JsEnv 链接 V8 静态库(wee8.lib / v8_11.8.172),也可切换为 QuickJS 或 Node.js 后端。

启动流程

1. V8 Isolate 初始化

FJsEnvImpl 构造函数(JsEnvImpl.cpp:347-646

  1. 创建 v8::Isolate,分配默认 ArrayBuffer 分配器
  2. 创建 v8::Context 存入 DefaultContext
  3. this 指针存为 Isolate 数据:Isolate->SetData(0, static_cast<IObjectMapper*>(this))

2. C++ → JS 桥接注册

构造函数注册 C++ 回调为全局 JavaScript 函数:

MethodBindingHelper<&FJsEnvImpl::EvalScript>::Bind(..., "__tgjsEvalScript", ...);
MethodBindingHelper<&FJsEnvImpl::SearchModule>::Bind(..., "__tgjsSearchModule", ...);
MethodBindingHelper<&FJsEnvImpl::LoadModule>::Bind(..., "__tgjsLoadModule", ...);
MethodBindingHelper<&FJsEnvImpl::FindModule>::Bind(..., "__tgjsFindModule", ...);
MethodBindingHelper<&FJsEnvImpl::LoadUEType>::Bind(..., puertsObj, "loadUEType", ...);
MethodBindingHelper<&FJsEnvImpl::LoadCppType>::Bind(..., puertsObj, "loadCPPType", ...);
MethodBindingHelper<&FJsEnvImpl::NewObjectByClass>::Bind(..., "__tgjsNewObject", ...);
MethodBindingHelper<&FJsEnvImpl::MakeUClass>::Bind(..., "__tgjsMakeUClass", ...);
MethodBindingHelper<&FJsEnvImpl::Mixin>::Bind(..., "__tgjsMixin", ...);
// + setTimeout, setInterval, dumpStatisticsLog, FNameToArrayBuffer 等

3. Bootstrap JS 模块(按顺序执行)

ExecuteModule("puerts/first_run.js");     // Inspector 存根(可选)
ExecuteModule("puerts/polyfill.js");      // JS polyfills非 NodeJS 模式)
ExecuteModule("puerts/log.js");           // console.log → UE_LOG
ExecuteModule("puerts/modular.js");       // CommonJS/ESM 模块系统
ExecuteModule("puerts/uelazyload.js");    // UE 类型懒加载 (Proxy)
ExecuteModule("puerts/events.js");        // EventEmitter
ExecuteModule("puerts/promises.js");      // Promise 支持
ExecuteModule("puerts/argv.js");          // 命令行参数
ExecuteModule("puerts/jit_stub.js");      // JIT 模块存根
ExecuteModule("puerts/hot_reload.js");    // V8 Inspector 热重载
ExecuteModule("puerts/pesaddon.js");      // PESAPI 原生 addon 加载

4. Start() 入口

FJsEnvImpl::Start() 调用 require(ModuleName) 执行用户入口模块。

模块解析系统

核心文件

  • puerts/modular.js — JS 端 CommonJS/ESM 模块系统
  • JSModuleLoader.h — C++ IJSModuleLoader 接口
  • DefaultJSModuleLoader.cpp — 文件系统解析实现

genRequire() 工厂

modular.js 实现 Node.js 兼容的 CommonJS 系统:

// require() 解析流程
1. 尝试 org_require (Node.js 原生如果使用 Node.js 后端)
2. 检查 buildinModule 缓存 ("ue", "puerts", "cpp")
3. 调用 __tgjsFindModule(moduleName)  搜索 PUERTS_MODULE 注册的 C++ addon
4. 调用 __tgjsSearchModule(moduleName, requiringDir)  文件系统搜索见下
5. 调用 __tgjsLoadModule(fullPath)  加载文件
6. CommonJS wrapper 执行: (function(exports, require, module, __filename, __dirname) { ... })
7. ES 模块 (.mjs/.mbc): 使用 V8 原生 v8::Module API

文件系统搜索(DefaultJSModuleLoader::Search

搜索算法:

  1. RequiredDir/RequiredModule 查找(尝试扩展名:.js, .mjs, .cjs, .mbc, .cbc, package.json, index.js
  2. RequiredDir/node_modules/RequiredModule 查找
  3. 向上遍历目录树,搜索每个父目录的 node_modules
  4. 回退到 Content/ScriptRoot/RequiredModule(默认 Content/JavaScript/
  5. 如果 ScriptRoot 不是 "JavaScript",也回退到 Content/JavaScript/

注意:搜索范围不包括插件 Content 目录。

JsEnv 数据流

puerts/uelazyload.js — UE 类型懒加载

这是最关键的模块之一:

  1. 创建全局 UE 对象作为 Proxy,访问属性时懒加载 UClass 类型
  2. 创建全局 CPP 对象用于注册的 C++ 类型
  3. UE 注册为内置模块:require('ue') 返回它
  4. 暴露:NewObject, NewStruct, FNameLiteral, NewArray/Set/Map, makeUClass, mixin, blueprint.tojs/load/unload
  5. 定义 UE 反射常量:FunctionFlags, ClassFlags, PropertyFlags
  6. Decorator 存根:uclass, ufunction, uproperty, uparam

puerts/callable.js(旧版/替代模式)

使用 sendRequestSync(同步 C++ 桥接)通过 JSON 序列化和 Proxy 包装调用 UE 方法。这是旧的 callable 模式。

puerts/hot_reload.js

使用 V8 Inspector 协议 (Debugger.setScriptSource) 在运行时热重载 JS 模块,无需重启游戏。触发 'HMR.prepare''HMR.finish' 事件。

puerts/pesaddon.js

包装 puerts.load() 加载原生 .dll/.dylib/.so addon使用 PESAPIPortable Embedded Scripting API

Puerts 模块(FPuertsModule

关键文件

  • PuertsModule.hIPuertsModule 接口
  • PuertsModule.cpp — 实现
  • PuertsSetting.h — 设置 UObject

StartupModule()

void FPuertsModule::StartupModule() {
    RegisterSettings();                    // 从 DefaultPuerts.ini 加载设置
    // Editor: 绑定 PIE PreBeginPIE/EndPIE 委托
    // 绑定热重载委托
    if (Settings.AutoModeEnable) {
        Enable();                          // 创建 JsEnv注册对象监听器
    }
}

MakeSharedJsEnv() — JsEnv 工厂

根据 UPuertsSetting 创建:

  • 单一 JsEnv (FJsEnv) — 普通模式
  • JsEnvGroup (FJsEnvGroup) — 多隔离模式2-9 个实例)

关键设置:

  • RootPath — JS 源码根目录(默认 "JavaScript",相对于 Content/
  • AutoModeEnable — 模块加载时自动启动
  • DebugEnable / DebugPort — V8 Inspector 调试
  • NumberOfJsEnv — 多隔离模式数量

UObject 创建监听

FPuertsModule 同时实现 FUObjectCreateListener,每个新创建的 UObject 触发 NotifyUObjectCreated()JsEnv->TryBindJs(InObject)。这就是 Puerts 自动将 TypeScript 行为注入 Blueprint 生成对象的机制。

TS → C++ 绑定架构

UTypeScriptGeneratedClassUTypeScriptBlueprint

  • UTypeScriptBlueprint — TypeScript 类的 Blueprint 资产类型
  • UTypeScriptGeneratedClass — TS 生成的运行时 UClass
    • DynamicInvokerTWeakPtr<ITsDynamicInvoker>,回调 FJsEnvImpl
    • FunctionToRedirect — 需要从 C++ 重定向到 JS 的函数名集合
    • HasConstructor — TS 类是否有 Constructor 方法
    • StaticConstructor — 替换原生 C++ 构造函数

UJSGeneratedFunction 和 Magic Bytecode

UJSGeneratedFunction 是动态创建的 UFunction,通过 magic bytecode trick 重定向到 JS

  1. 在 UFunction 的 Script bytecode 中存储自身指针3 字节 magic marker + 8 字节指针)
  2. execCallJS 执行时恢复指针
  3. 调用 DynamicInvoker->InvokeJsMethod()FJsEnvImpl::InvokeJsMethod()
  4. FunctionTranslator->CallJs() 将所有参数编组到 V8调用 JS 函数

MakeUClass / Mixin — 运行时类创建

  • MakeUClass (puerts.makeUClass(ctor)):接收 JS 构造函数、原型、类名、方法对象、父 UClass创建 UJSGeneratedClass,遍历父类 FUNC_BlueprintEvent,为每个覆盖创建 UJSGeneratedFunction
  • Mixin (puerts.blueprint.mixin(to, mixinMethods)):接收现有 UClass添加方法覆盖

C++ 调用 TS 的方式

方式 1FJsObject(直接 JS 函数包装器)

// FJsObject 是 USTRUCT持有持久化 V8 引用
FJsObject MyFunc;
MyFunc.Action<int32, FString>(42, TEXT("hello"));      // 无返回值
int32 Result = MyFunc.Func<int32, int32, FString>(42, TEXT("hello"));  // 有返回值
int32 Val = MyObj.Get<int32>("someProperty");           // 读取属性
MyObj.Set("someProperty", 42);                           // 设置属性

自动使用 puerts::v8_impl::Converter<T> 进行 C++ ↔ V8 类型转换。线程安全(THREAD_SAFE 模式下使用 v8::Locker)。

方式 2UFUNCTION / BlueprintEvent 覆盖

标准 UE 模式:

// C++ 中定义
UFUNCTION(BlueprintImplementableEvent)
void OnSomethingHappened(int32 Param);

// TypeScript 中实现
class MyActor extends UE.Actor {
    OnSomethingHappened(Param: number): void {
        // 被 C++ 调用时执行
    }
}

方式 3委托

import { toDelegate, toManualReleaseDelegate } from "puerts";
let d = toDelegate(myObject, myCallback);           // 绑定到对象
let d2 = toManualReleaseDelegate(myCallback);       // 独立委托

C++ 侧,FJsEnvImpl 维护 DelegateMap 将委托指针映射到 JS 回调函数。

方式 4ITsDynamicInvoker 接口

  • IDynamicInvoker::InvokeJsMethod — 从 UJSGeneratedFunction::execCallJS 调用
  • IDynamicInvoker::JsConstruct — 运行 JS 构造函数
  • ITsDynamicInvoker::TsConstruct — TS 特定构造路径

FJsEnvImpl 通过内部类 DynamicInvokerImplTsDynamicInvokerImpl 实现这些接口。

方式 5PUERTS_MODULEAddon 模块系统)

C++ 代码注册为 addon 模块:

// 在任何 C++ 文件中
PUERTS_MODULE(MyPlugin, [](v8::Local<v8::Context> Context, v8::Local<v8::Object> Exports) {
    // 注册类、函数等
    Exports->Set(...);
});

使用静态自动注册:

#define PUERTS_MODULE(Name, RegFunc) \
    static struct FAutoRegisterFor##Name { \
        FAutoRegisterFor##Name() { \
            PUERTS_NAMESPACE::RegisterAddon(#Name, (RegFunc)); \
        } \
    } _AutoRegisterFor##Name

JS 端:require('MyPlugin')FindModule → 执行注册函数 → 获取 exports。

类型声明文件

位于 puerts\unreal\Puerts\Typing\

文件 内容
puerts/index.d.ts 核心 Puerts API$Ref, blueprint, toDelegate, on/off/emit, merge, load
ue/puerts.d.ts UE 绑定类型:$Delegate, TArray, TSet, TMap, NewObject, NewStruct
ue/puerts_decorators.d.ts 装饰器:rpc.flags(), edit_on_instance(), uclass, ufunction
ue/index.d.ts 聚合所有声明(通过 /// <reference path="..." />

ue.d.tsue_bp.d.tsDeclarationGenerator 模块使用 UE 反射系统自动生成。

架构图

[Game/Editor 启动]
       │
       ▼
FPuertsModule::StartupModule()
  ├── RegisterSettings() → 读取 DefaultPuerts.ini
  └── Enable()
       ├── GUObjectArray::AddUObjectCreateListener(this)  // 拦截所有 UObject 创建
       └── MakeSharedJsEnv()
            └── FJsEnv(ScriptRoot) 包装器
                 └── FJsEnvImpl 构造
                      ├── v8::Isolate::New() + v8::Context::New()
                      ├── 注册全局 C++ 回调 (__tgjs* + puerts.*)
                      ├── ExecuteModule() ×13 (bootstrap JS 文件)
                      └── 捕获 global.require, global.__reload
                 └── Start(ModuleName) → require(ModuleName) → 用户代码执行
                      │
                      ▼
[运行时: UObject 创建(如 Blueprint 实例化)]
       │
       ▼
FPuertsModule::NotifyUObjectCreated()
  └── JsEnv->TryBindJs(InObject)
       ├── TypeScriptGeneratedClass? → MakeSureInject (绑定 JS 原型)
       └── JSGeneratedClass? → JsConstruct (调用 JS 构造函数)

[C++ 调用 TS]
  UFUNCTION(BlueprintEvent) _Implementation → UE VM → execCallJS
    → DynamicInvoker->InvokeJsMethod()
      → FJsEnvImpl::InvokeJsMethod()
        → FunctionTranslator->CallJs() [参数编组]
          → JS 函数被调用

[TS 调用 C++]
  require('ue') → Proxy → loadUEType → UETypeToJsClass → DataTransfer::FindOrAddObject
  new MyActor() → NewObjectByClass → UObject 创建并调用构造函数
  actor.K2_SomeMethod() → UFUNCTION → 原生 C++ 代码执行

关键文件索引

文件 用途
unreal/Puerts/Puerts.uplugin 插件定义(模块列表、加载阶段)
Source/Puerts/Public/PuertsModule.h IPuertsModule 接口
Source/Puerts/Private/PuertsModule.cpp 模块启动/关闭JsEnv 生命周期PIE 钩子
Source/JsEnv/Public/JsEnv.h FJsEnv 公开包装器
Source/JsEnv/Private/JsEnvImpl.h FJsEnvImpl 完整定义
Source/JsEnv/Private/JsEnvImpl.cpp V8 初始化、bootstrap、模块执行、绑定
Source/JsEnv/Public/JSModuleLoader.h IJSModuleLoader 接口 + DefaultJSModuleLoader
Source/JsEnv/Private/DefaultJSModuleLoader.cpp 文件系统搜索/加载实现
Source/JsEnv/Public/JsObject.h FJsObject — JS 函数/对象的 C++ 包装器
Source/JsEnv/Public/DataTransfer.h DataTransfer — C++/JS 类型转换
Source/JsEnv/Public/DynamicInvoker.h IDynamicInvoker 接口
Source/JsEnv/Public/TsDynamicInvoker.h ITsDynamicInvoker 接口
Source/JsEnv/Public/TypeScriptGeneratedClass.h UTypeScriptGeneratedClass
Source/JsEnv/Private/JSGeneratedClass.h UJSGeneratedClass
Source/JsEnv/Private/JSGeneratedFunction.h UJSGeneratedFunction (magic bytecode)
Source/JsEnv/Private/FunctionTranslator.h FFunctionTranslator — C++/JS 参数编组
Source/JsEnv/Public/JSClassRegister.h JSClassDefinition, PUERTS_MODULE
Source/JsEnv/Public/JsEnvGroup.h FJsEnvGroup — 多隔离模式
Content/JavaScript/puerts/modular.js JS 端 CommonJS/ESM 模块系统
Content/JavaScript/puerts/uelazyload.js UE 类型懒加载, makeUClass, mixin
Content/JavaScript/puerts/callable.js 替代 Proxy callable 模式
Content/JavaScript/puerts/hot_reload.js V8 Inspector 热重载
Content/JavaScript/puerts/pesaddon.js PESAPI 原生 addon 加载器