Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions conv/j2t/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# JSON to Thrift (j2t) Converter

`conv/j2t` 包实现了将 JSON 数据高效转换为 Thrift 二进制数据的核心逻辑。该包充分利用了 SIMD(如 AVX2)、JIT 编译及底层汇编优化以实现极高的序列化性能,同时也提供 Fallback 机制保障跨平台的兼容性。

## 核心设计与数据结构

- **`BinaryConv`**:核心转换器,配置了 `conv.Options` (如 `EnableHttpMapping`, `String2Int64` 等)。对外提供 `Do` 和 `DoInto` 等 API 进行数据转换。
- **配置与 Flags**:将上层 `conv.Options` 中如默认字段输出、未知字段处理等映射为 uint64 的 `types.F_*` 标志位传入底层执行逻辑,以指导 JIT 和原生函数的行为。

## 核心流程流转

```mermaid
graph TD
A[Start: Do / DoInto] --> B{EnableHttpMapping?}
B -- Yes --> C[Extract HTTP Request]
B -- No --> D[Arch Routing / do]
C --> D

D -->|AMD64 CPU| E[FFI Route: impl_amd64.go]
D -->|Other / Fallback| F[Go Route: impl_fallback.go]

subgraph AMD64[AMD64 C/FFI Path]
E --> G[Sonic Loader WrapGoC]
G --> H[__j2t_fsm_exec in native/thrift.c]
H --> I[C Flat State Machine]
I --> J[SIMD Acceleration & Direct Buffer Write]
end

subgraph GO_FALLBACK[Go Fallback Path]
F --> K[doRecurse / 递归解析]
K --> L{JSON Node Type}

L -->|Object| M{STRUCT or MAP?}
M -->|STRUCT| N[O1 匹配: FieldByKey Trie/Hash]
M -->|MAP| O[打桩占位: WriteMapBegin]
O --> P[回填容量: ModifyI32]

L -->|Array| Q[打桩占位: WriteListBegin]
Q --> R[回填容量: ModifyI32]

L -->|Scalar| S[Type Cast / Base64 Decode]

N --> K
S --> T[Write Thrift Primitive]
end

J --> Z[(Thrift Binary Buffer)]
P --> Z
R --> Z
T --> Z
```

1. **入口函数 `Do` / `DoInto`** (在 `conv.go` 中)
接收 JSON 字节流、目标 Thrift 描述符 (`thrift.TypeDescriptor`) 以及 Context,初始化相关的缓存 `buf` 并提取 HTTP Mapping 对应的 Request。
2. **底层路由 `do`** (平台自适应在 `impl_amd64.go` 或 `impl_fallback.go` 中)
根据系统架构采用不同的底层实现:
- **AMD64 下**:触发 FFI 层原生的 C/汇编调用,如果 JSON 数据超过一定长度且类型不是纯标量,还会开启 JSON ASM 验证机制。内部调用底层的 `__cgo_json_to_thrift` 指针进行解析。在解析前完成 JSON 解析器的状态机分配与准备。
- **Fallback 下** (其他架构如 ARM64):通过构建自定义状态机 (`_Context`) 在 Go 层逐步递归地解析 JSON,并通过匹配 Thrift 的类型树 (`TypeDescriptor`) 将对应元素编码为 Thrift 二进制并写入结果缓冲。

## HTTP Mapping 处理

在业务网关场景中,往往需要从 HTTP 请求头、Query、Cookie 中获取参数,而非仅从 JSON Body。`j2t` 提供 `HTTP 映射` 功能(通过 `EnableHttpMapping`):
1. 在 `do` 函数中检测是否需处理 HTTP 参数。如果开启,则解析描述符检查请求需要的必须和可选字段。
2. 通过 `http_conv.go` 提供逻辑,利用 `http.RequestGetter` 从 URL 参数、HTTP Header 和 Cookie 提取数据。
3. 这些数据会被按正确的 Thrift 类型包裹(在 JSON 解析之余)写入最后的数据缓冲中。

## AMD64 & SIMD 优化与 FFI 交互架构

在具有高级指令集(如 AMD64架构下的 AVX/AVX2/SSE)环境时,`dynamicgo` 为追求极致的序列化性能,实现了深度优化的 C 语言扩展,并通过自定义的 FFI(Foreign Function Interface)机制与 Go 层交互:

### 1. C 代码层核心逻辑 (`native/thrift.c`)
- **零分配扁平化状态机 (`j2t_fsm_exec`)**:
C 层实现了名为 `j2t_fsm_exec` 的核心解析函数。由于 C 的原生递归在深度较大时可能会导致栈溢出,该函数基于自身维护的数据结构 `J2TStateMachine`,使用预分配的数组 (`self->vt`) 模拟堆栈 (`sp`) 的压栈和弹栈。外层通过一个高效的 `while(self->sp)` 循环处理。
- **流式 JSON 解析与底层序列化**:
函数在循环中通过 `advance_ns` 步进解析 JSON 字符串(跳过空白符等),然后通过 `J2T_ST(st)` 宏(如 `J2T_ARR_0`, `J2T_OBJ_0`, `J2T_VAL` 等)匹配当前 JSON 的结构状态节点。
在解析出 JSON Key 或 Value 后,立刻基于传入的 `tTypeDesc` (Thrift 类型描述符映射),在 C 层调用对应的 `tb_write_XXX` 系列函数,直接向预分配的 `GoSlice *buf` 二进制写入 Thrift 的 TType 标记、Field ID、以及对应的值(如 int, string 乃至进行 Base64 转换)。
- **SIMD/位运算加速支持**:
在遇到复杂的字符串转义(如 `quote`,`unquote`)和数值解析(如 `atof`)等计算密集型节点时,借助于 `native/*.c` 里包装的位运算甚至是 SIMD 指令进行极大加速,显著由于纯 Go 层的解析性能。

### 2. C 与 Go 的 FFI 交互机制 (`internal/native/dispatch_amd64.go`)
- **旁路 Cgo 的高性能跳板 (Sonic Loader)**:
因为原生的 Cgo 调用有着数十甚至百纳秒级别的上下文切换及协程(G)状态锁定开销,`dynamicgo` 在这里直接弃用了标准的 cgo 方案。
- **纯汇编入口绑定 (`loader.WrapGoC`)**:
框架在 Go 层导入字节跳动的开源库 `sonic/loader`,它将编译好的不同指令集架构的 C 语言目标文件段(如 `avx2/native.c`)动态加载进 Go 运行时的内存空间并将其链接为 Go 函数。
```go
func J2T_FSM(fsm *types.J2TStateMachine, buf *[]byte, src *string, flag uint64) (ret uint64) {
return __j2t_fsm_exec(rt.NoEscape(unsafe.Pointer(fsm)), rt.NoEscape(unsafe.Pointer(buf)), rt.NoEscape(unsafe.Pointer(src)), flag)
}
```
如上所示,当 Go 层通过 `J2T_FSM` 传入配置好的有限状态机 (`fsm`)、JSON 原文 (`src`) 以及目标缓冲 (`buf`) 的指针时,Go 把对应的底层内存以 `NoEscape` 原语直接借给 C 端;C 端执行完毕对缓冲结构体 `GoSlice` 的填充后,Go 层即可零损耗地获得序列化后的 Thrift 二进制流。

## 如何编译 C 代码

C 代码更新或 debug 时候需要重新编译 asm 文件,步骤如下:
1. 安装 clang-14
```
brew install llvm@14
```
2. dynamicgo 根目录下下载 `tools/asm2asm` submodule
```
git submodule update --init --remote -f
```
3. 执行 `make`,会自动编译生成 `internal/nativx/xxx`(包含 AVX2、AVX、SSE 三个版本的 C 语言目标文件)以及对应的 Go 汇编绑定代码。
4. PS:如果需要在执行 C 时打印 debug log,需要在 makefile 中**编译flags**添加 `-DDEBUG` 定义。
```
CFLAGS_avx := -msse -mno-sse4 -mavx -mpclmul -mno-avx2 -mstack-alignment=0 -DDEBUG -DUSE_AVX=1 -DUSE_AVX2=0
CFLAGS_avx2 := -msse -mno-sse4 -mavx -mpclmul -mavx2 -mstack-alignment=0 -DDEBUG -DUSE_AVX=1 -DUSE_AVX2=1
CFLAGS_sse := -msse -mno-sse4 -mno-avx -mno-avx2 -mpclmul -DDEBUG
```


## 总结

`conv/j2t` 包通过灵活的双重实现架构(Go层手工递归解包 vs 底层 AVX C/ASM 加速)、与描述符的强绑定以及智能的 HTTP 参数映射,在复杂和高吞吐的微服务网关场景中提供了一套极致性能的 JSON 转 Thrift 的序列化方案。
95 changes: 95 additions & 0 deletions trim/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Trim Package

`trim` 是 `dynamicgo` 中的一个核心的数据处理包,主要提供对象级别的数据裁剪(Pruning / Fetch)与按需赋值(Assign)的能力。其设计初衷是能以一套统一的 DSL (描述符) 高效处理嵌套层次较深的复杂 Go 结构体(特别是通过 Protobuf 和 Thrift 自动生成的结构化数据),实现按需保留所需字段、并将其逆向赋值组回的功能。

## 1. 核心功能及概念

`trim` 包围绕三个核心抽象展开:

1. **Descriptor(描述符)**:通过树状结构描述所需保留或访问的特定路径与字段(类似于 GraphQL 的选择树)。
2. **Fetcher(提取器)**:顺着 Descriptor 描述的路径,以反射的方式将源(结构体 / Map / List 等)中特定数据抽取出来。
3. **Assigner(赋值器)**:提取过程的逆向应用——根据 Descriptor 的树状结构,将零散结构化数据(如通过 Fetch 得到的嵌套 Map/Slice)自动精准映射赋值到目标强类型 Go 对象中,并专门处理了 Protobuf 的未知字段。

---

## 2. 核心原理与实现细节

### 2.1 Descriptor (模型映射描述)
位于 `desc.go`。
- **设计**:由特定的类型种类 `Kind` (支持 `Leaf` / `Struct` / `StrMap` / `List`) 和一组 `Children` 字段组成。例如描述提取字典的键或对应的 ID 字段。
- **循环引用支持**:提供了 `Normalize()` 方法(此方法被调用后 Descriptor 才会变为可用状态)。在其内部会对具有指向自身(或者内部构成环)的图状描述符网络进行缓存和解析,防脱轨或无尽循环。
- **序列化监控**:内置 `String()` 和 `MarshalJSON()` 支持以文本格式探查复杂 Descriptor 的全貌。

### 2.2 Fetcher (数据裁剪/提取)
位于 `fetch.go`。
- **底层机制**:基于 `reflect` 实施深层对象的遍历。
- **高速缓存**:对于 Thrift 或 Protobuf 结构体,`Fetcher` 不会每次都动态解析 struct tag。它全局维护了一个 `fieldCache` 字典 (`sync.Map`),一次解析字段与 Tag 之后(包括解析 Protobuf Name、Thrift Field ID 等),就将结构体的索引记录到缓存,之后以 O(1) 的开销命中字段的 `reflect.Value`。
- **未知字段提取**:内置对 thrift `_unknownFields` 等未知域字段的读取兼容,以防止在降级/动态处理时漏掉非强类型的字段。可以使用 `SetThriftUnknownFieldName()` 灵活重定义配置。

### 2.3 Assigner (按需赋值映射)
位于 `assign.go`。
- **尽力而为(try-best mode)策略**:赋值器遇到单条字段的类型不匹配或缺失时,不仅不会立刻中断进程,还会将其记录进一个 `errorCollector` 中并继续执行,最后通过 `MultiErrors` 打包返回所有的错误(或者可通过 `AssignOptions.DisallowNotDefined` 提供更严苛的拦截策略)。
- **Protobuf Unknown Fields 兜底**:如果在源数据中提取到了不在实际 Go `struct` 定义内的字段(例如高版本的 protobuf 数据赋值降级到低版本的 Go 结构体):
- 若相应的未知字段名存在于 Descriptor 的元数据内,Assigner 会将数据序列化为 protobuf Binary wire 结构并放至目标结构体的 `XXX_unrecognized` 字段。
- 对于未分类(Unkeyed)字面量,支持注入至 `XXX_NoUnkeyedLiteral`(也可以由 `SetPB***FieldName` 函数重新指定)。
- **零分配和对象池化(对象复用)**:为了在递归过程中降低性能损耗,在栈内跟踪 (Path stack / frames) 广泛使用了资源池(`sync.Pool`)复用,减少了不必要的 GC 开销。

---

## 3. 使用示例

```go
package main

import (
"fmt"
"github.com/cloudwego/dynamicgo/trim"
)

// 以一个生成的 Protobuf 消息结构体为例
type User struct {
ID int `protobuf:"varint,1,req,name=id"`
Name string `protobuf:"bytes,2,opt,name=name"`
}

func main() {
// 1. 构建数据抓取描述符 Descriptor: 这里意图仅保留 ID
desc := &trim.Descriptor{
Kind: trim.TypeKind_Struct,
Type: "User",
Children: []trim.Field{
{Name: "id", ID: 1, Desc: &trim.Descriptor{Kind: trim.TypeKind_Leaf}},
},
}
// WARNING: 构建完毕必须调用 Normalize() 处理树的搜索索引
desc.Normalize()

// 原始需要被裁剪的数据
user := &User{ID: 10086, Name: "Alice"}

// 2. 使用 FetchAny (裁剪)
fetcher := trim.Fetcher{}
fetchedResult, err := fetcher.FetchAny(desc, user)
if err != nil {
panic(err)
}

// fetchedResult 中目前仅包含要求的 ID 字段了
// 输出: Fetch 结果: map[id:10086]
fmt.Printf("Fetch 结果: %+v\n", fetchedResult)

// 3. 使用 AssignAny (重新映射到空数据体)
newUser := &User{}
assigner := trim.Assigner{}

// 从前一级裁切获得的 object (如 map) 填入到 newUser 结构中
err = assigner.AssignAny(desc, fetchedResult, newUser)
if err != nil {
panic(err)
}

// newUser 被赋予了部分值
// 输出: Assign 结果: &{ID:10086 Name:}
fmt.Printf("Assign 结果: %+v\n", newUser)
}
```
Loading