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

364 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags:
- UE
- Puerts
- TypeScript
- V8
- 架构分析
created: 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 函数:
```cpp
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 模块(按顺序执行)
```cpp
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 系统:
```javascript
// 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.h``IPuertsModule` 接口
- `PuertsModule.cpp` — 实现
- `PuertsSetting.h` — 设置 UObject
### `StartupModule()`
```cpp
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++ 绑定架构
### `UTypeScriptGeneratedClass` 和 `UTypeScriptBlueprint`
- `UTypeScriptBlueprint` — TypeScript 类的 Blueprint 资产类型
- `UTypeScriptGeneratedClass` — TS 生成的运行时 UClass
- `DynamicInvoker``TWeakPtr<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 的方式
### 方式 1`FJsObject`(直接 JS 函数包装器)
```cpp
// 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 模式:
```cpp
// C++ 中定义
UFUNCTION(BlueprintImplementableEvent)
void OnSomethingHappened(int32 Param);
// TypeScript 中实现
class MyActor extends UE.Actor {
OnSomethingHappened(Param: number): void {
// 被 C++ 调用时执行
}
}
```
### 方式 3委托
```typescript
import { toDelegate, toManualReleaseDelegate } from "puerts";
let d = toDelegate(myObject, myCallback); // 绑定到对象
let d2 = toManualReleaseDelegate(myCallback); // 独立委托
```
C++ 侧,`FJsEnvImpl` 维护 `DelegateMap` 将委托指针映射到 JS 回调函数。
### 方式 4`ITsDynamicInvoker` 接口
- `IDynamicInvoker::InvokeJsMethod` — 从 `UJSGeneratedFunction::execCallJS` 调用
- `IDynamicInvoker::JsConstruct` — 运行 JS 构造函数
- `ITsDynamicInvoker::TsConstruct` — TS 特定构造路径
`FJsEnvImpl` 通过内部类 `DynamicInvokerImpl``TsDynamicInvokerImpl` 实现这些接口。
### 方式 5`PUERTS_MODULE` 宏Addon 模块系统)
C++ 代码注册为 addon 模块:
```cpp
// 在任何 C++ 文件中
PUERTS_MODULE(MyPlugin, [](v8::Local<v8::Context> Context, v8::Local<v8::Object> Exports) {
// 注册类、函数等
Exports->Set(...);
});
```
使用静态自动注册:
```cpp
#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.ts``ue_bp.d.ts``DeclarationGenerator` 模块使用 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 加载器 |