vault backup: 2026-05-31 15:35:41
This commit is contained in:
363
07-Other/AI/AI Agent/UnrealEngine/Puerts/Puerts 架构深度分析.md
Normal file
363
07-Other/AI/AI Agent/UnrealEngine/Puerts/Puerts 架构深度分析.md
Normal file
@@ -0,0 +1,363 @@
|
||||
---
|
||||
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,使用 PESAPI(Portable 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`)。
|
||||
|
||||
### 方式 2:UFUNCTION / 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 加载器 |
|
||||
Reference in New Issue
Block a user