用 Rust 封装 XTP C++ 接口:从零到能用的完整教程

说明:这个是我花了近300元的opencode调用Kimi K3的实践。全程AI 制作,谨供参考。
面向读者 :会写 Rust 基本代码、但没做过 FFI(外部函数接口)的实习生/初学者。
读完你将获得 :一个能真实连接中泰证券 XTP 柜台、订阅行情、下单查询的 Rust 库,以及一套"任何 C++ 接口都能照此封装"的方法论。
配套资料 :XTP 官方文档 https://xtp.zts.com.cn/doc/api/xtpDoc、SDK 头文件(
xtp_trader_api.h、xtpx_quote_api.h等)。本文代码:全部来自一个通过 23 项测试(含真实 DLL 冒烟测试)的可用工程,关键片段可直接复制。
目录
- [第 0 章 我们要做什么、为什么难](#第 0 章 我们要做什么、为什么难)
- [第 1 章 认识 XTP API 的构造](#第 1 章 认识 XTP API 的构造)
- [第 2 章 补课:ABI、调用约定与 vtable](#第 2 章 补课:ABI、调用约定与 vtable)
- [第 3 章 工程搭建与 DLL 加载](#第 3 章 工程搭建与 DLL 加载)
- [第 4 章 重建 vtable:调用第一个 C++ 虚函数](#第 4 章 重建 vtable:调用第一个 C++ 虚函数)
- [第 5 章 结构体逐字段映射(含对齐/pack/union/位域)](#第 5 章 结构体逐字段映射(含对齐/pack/union/位域))
- [第 6 章 枚举与常量映射](#第 6 章 枚举与常量映射)
- [第 7 章 回调(SPI)桥:在 Rust 里伪造一个 C++ 对象](#第 7 章 回调(SPI)桥:在 Rust 里伪造一个 C++ 对象)
- [第 8 章 安全封装层设计](#第 8 章 安全封装层设计)
- [第 9 章 踩坑实录:21 个真实 bug 的分类清单](#第 9 章 踩坑实录:21 个真实 bug 的分类清单)
- [第 10 章 测试与调试方法论](#第 10 章 测试与调试方法论)
- [第 11 章 完整使用示例](#第 11 章 完整使用示例)
- [第 12 章 连接、账号与生命周期(官方规则)](#第 12 章 连接、账号与生命周期(官方规则))
- [第 13 章 订单与查询的业务语义](#第 13 章 订单与查询的业务语义)
- [第 14 章 行情数据语义与生产环境调优](#第 14 章 行情数据语义与生产环境调优)
- [第 15 章 风控、错误代码与下单参数速查](#第 15 章 风控、错误代码与下单参数速查)
- [第 16 章 综合实战:最小可用行情接收器](#第 16 章 综合实战:最小可用行情接收器)
- [第 17 章 XTPX 4.0 与经典版 API 的差异](#第 17 章 XTPX 4.0 与经典版 API 的差异)
- [附录 A 速查表](#附录 A 速查表)
- [附录 B 术语表](#附录 B 术语表)
- [附录 C 常用错误代码表](#附录 C 常用错误代码表)
- [附录 D 下单参数组合速查表](#附录 D 下单参数组合速查表)
- [附录 E 官方文档索引](#附录 E 官方文档索引)
第 0 章 我们要做什么、为什么难
0.1 任务一句话
中泰证券 XTP(极速交易平台)只提供 C++ 接口 (xtptraderapi.dll 交易、xtpxquoteapi.dll 行情)。我们要在 Rust 里使用它,并且做到:
- 调得动:创建 API、登录、订阅、查询、下单;
- 收得到 :行情推送、报单回报等回调能进 Rust 代码;
- 不崩溃、不 UB:内存布局、生命周期、线程安全全部正确。
0.2 为什么不是"写个 extern 就完事"
很多教程告诉你 FFI 就是:
rust
extern "C" { fn some_c_function(x: i32) -> i32; }
但 XTP 给的不是一组 C 函数,而是 C++ 类:
cpp
// xtp_trader_api.h(简化)
namespace XTP { namespace API {
class TraderApi {
public:
static TraderApi* CreateTraderApi(uint8_t client_id, const char* path, XTP_LOG_LEVEL level);
virtual void Release() = 0;
virtual const char* GetApiVersion() = 0;
virtual uint64_t Login(const char* ip, int port, ...) = 0;
virtual uint64_t InsertOrder(XTPOrderInsertInfo* order, uint64_t session_id) = 0;
// ... 一共 80+ 个虚函数
};
class TraderSpi { // 回调接口,要求我们"实现"后注册进去
virtual void OnOrderEvent(XTPOrderInfo*, XTPRI*, uint64_t) {};
// ... 一共 65 个虚函数
};
}}
这带来四座大山:
#mermaid-svg-DuyySu6dBz6qf16b{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DuyySu6dBz6qf16b .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DuyySu6dBz6qf16b .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DuyySu6dBz6qf16b .error-icon{fill:#552222;}#mermaid-svg-DuyySu6dBz6qf16b .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DuyySu6dBz6qf16b .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DuyySu6dBz6qf16b .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DuyySu6dBz6qf16b .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DuyySu6dBz6qf16b .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DuyySu6dBz6qf16b .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DuyySu6dBz6qf16b .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DuyySu6dBz6qf16b .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DuyySu6dBz6qf16b .marker.cross{stroke:#333333;}#mermaid-svg-DuyySu6dBz6qf16b svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DuyySu6dBz6qf16b p{margin:0;}#mermaid-svg-DuyySu6dBz6qf16b .edge{stroke-width:3;}#mermaid-svg-DuyySu6dBz6qf16b .section--1 rect,#mermaid-svg-DuyySu6dBz6qf16b .section--1 path,#mermaid-svg-DuyySu6dBz6qf16b .section--1 circle,#mermaid-svg-DuyySu6dBz6qf16b .section--1 polygon,#mermaid-svg-DuyySu6dBz6qf16b .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .section--1 text{fill:#ffffff;}#mermaid-svg-DuyySu6dBz6qf16b .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-DuyySu6dBz6qf16b .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .edge-depth--1{stroke-width:17;}#mermaid-svg-DuyySu6dBz6qf16b .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-DuyySu6dBz6qf16b .disabled,#mermaid-svg-DuyySu6dBz6qf16b .disabled circle,#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:lightgray;}#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:#efefef;}#mermaid-svg-DuyySu6dBz6qf16b .section-0 rect,#mermaid-svg-DuyySu6dBz6qf16b .section-0 path,#mermaid-svg-DuyySu6dBz6qf16b .section-0 circle,#mermaid-svg-DuyySu6dBz6qf16b .section-0 polygon,#mermaid-svg-DuyySu6dBz6qf16b .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-DuyySu6dBz6qf16b .section-0 text{fill:black;}#mermaid-svg-DuyySu6dBz6qf16b .node-icon-0{font-size:40px;color:black;}#mermaid-svg-DuyySu6dBz6qf16b .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-DuyySu6dBz6qf16b .edge-depth-0{stroke-width:14;}#mermaid-svg-DuyySu6dBz6qf16b .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-DuyySu6dBz6qf16b .disabled,#mermaid-svg-DuyySu6dBz6qf16b .disabled circle,#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:lightgray;}#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:#efefef;}#mermaid-svg-DuyySu6dBz6qf16b .section-1 rect,#mermaid-svg-DuyySu6dBz6qf16b .section-1 path,#mermaid-svg-DuyySu6dBz6qf16b .section-1 circle,#mermaid-svg-DuyySu6dBz6qf16b .section-1 polygon,#mermaid-svg-DuyySu6dBz6qf16b .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .section-1 text{fill:black;}#mermaid-svg-DuyySu6dBz6qf16b .node-icon-1{font-size:40px;color:black;}#mermaid-svg-DuyySu6dBz6qf16b .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .edge-depth-1{stroke-width:11;}#mermaid-svg-DuyySu6dBz6qf16b .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-DuyySu6dBz6qf16b .disabled,#mermaid-svg-DuyySu6dBz6qf16b .disabled circle,#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:lightgray;}#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:#efefef;}#mermaid-svg-DuyySu6dBz6qf16b .section-2 rect,#mermaid-svg-DuyySu6dBz6qf16b .section-2 path,#mermaid-svg-DuyySu6dBz6qf16b .section-2 circle,#mermaid-svg-DuyySu6dBz6qf16b .section-2 polygon,#mermaid-svg-DuyySu6dBz6qf16b .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .section-2 text{fill:#ffffff;}#mermaid-svg-DuyySu6dBz6qf16b .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-DuyySu6dBz6qf16b .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .edge-depth-2{stroke-width:8;}#mermaid-svg-DuyySu6dBz6qf16b .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-DuyySu6dBz6qf16b .disabled,#mermaid-svg-DuyySu6dBz6qf16b .disabled circle,#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:lightgray;}#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:#efefef;}#mermaid-svg-DuyySu6dBz6qf16b .section-3 rect,#mermaid-svg-DuyySu6dBz6qf16b .section-3 path,#mermaid-svg-DuyySu6dBz6qf16b .section-3 circle,#mermaid-svg-DuyySu6dBz6qf16b .section-3 polygon,#mermaid-svg-DuyySu6dBz6qf16b .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .section-3 text{fill:black;}#mermaid-svg-DuyySu6dBz6qf16b .node-icon-3{font-size:40px;color:black;}#mermaid-svg-DuyySu6dBz6qf16b .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .edge-depth-3{stroke-width:5;}#mermaid-svg-DuyySu6dBz6qf16b .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-DuyySu6dBz6qf16b .disabled,#mermaid-svg-DuyySu6dBz6qf16b .disabled circle,#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:lightgray;}#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:#efefef;}#mermaid-svg-DuyySu6dBz6qf16b .section-4 rect,#mermaid-svg-DuyySu6dBz6qf16b .section-4 path,#mermaid-svg-DuyySu6dBz6qf16b .section-4 circle,#mermaid-svg-DuyySu6dBz6qf16b .section-4 polygon,#mermaid-svg-DuyySu6dBz6qf16b .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .section-4 text{fill:black;}#mermaid-svg-DuyySu6dBz6qf16b .node-icon-4{font-size:40px;color:black;}#mermaid-svg-DuyySu6dBz6qf16b .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .edge-depth-4{stroke-width:2;}#mermaid-svg-DuyySu6dBz6qf16b .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-DuyySu6dBz6qf16b .disabled,#mermaid-svg-DuyySu6dBz6qf16b .disabled circle,#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:lightgray;}#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:#efefef;}#mermaid-svg-DuyySu6dBz6qf16b .section-5 rect,#mermaid-svg-DuyySu6dBz6qf16b .section-5 path,#mermaid-svg-DuyySu6dBz6qf16b .section-5 circle,#mermaid-svg-DuyySu6dBz6qf16b .section-5 polygon,#mermaid-svg-DuyySu6dBz6qf16b .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .section-5 text{fill:black;}#mermaid-svg-DuyySu6dBz6qf16b .node-icon-5{font-size:40px;color:black;}#mermaid-svg-DuyySu6dBz6qf16b .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .edge-depth-5{stroke-width:-1;}#mermaid-svg-DuyySu6dBz6qf16b .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-DuyySu6dBz6qf16b .disabled,#mermaid-svg-DuyySu6dBz6qf16b .disabled circle,#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:lightgray;}#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:#efefef;}#mermaid-svg-DuyySu6dBz6qf16b .section-6 rect,#mermaid-svg-DuyySu6dBz6qf16b .section-6 path,#mermaid-svg-DuyySu6dBz6qf16b .section-6 circle,#mermaid-svg-DuyySu6dBz6qf16b .section-6 polygon,#mermaid-svg-DuyySu6dBz6qf16b .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .section-6 text{fill:black;}#mermaid-svg-DuyySu6dBz6qf16b .node-icon-6{font-size:40px;color:black;}#mermaid-svg-DuyySu6dBz6qf16b .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .edge-depth-6{stroke-width:-4;}#mermaid-svg-DuyySu6dBz6qf16b .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-DuyySu6dBz6qf16b .disabled,#mermaid-svg-DuyySu6dBz6qf16b .disabled circle,#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:lightgray;}#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:#efefef;}#mermaid-svg-DuyySu6dBz6qf16b .section-7 rect,#mermaid-svg-DuyySu6dBz6qf16b .section-7 path,#mermaid-svg-DuyySu6dBz6qf16b .section-7 circle,#mermaid-svg-DuyySu6dBz6qf16b .section-7 polygon,#mermaid-svg-DuyySu6dBz6qf16b .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .section-7 text{fill:black;}#mermaid-svg-DuyySu6dBz6qf16b .node-icon-7{font-size:40px;color:black;}#mermaid-svg-DuyySu6dBz6qf16b .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .edge-depth-7{stroke-width:-7;}#mermaid-svg-DuyySu6dBz6qf16b .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-DuyySu6dBz6qf16b .disabled,#mermaid-svg-DuyySu6dBz6qf16b .disabled circle,#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:lightgray;}#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:#efefef;}#mermaid-svg-DuyySu6dBz6qf16b .section-8 rect,#mermaid-svg-DuyySu6dBz6qf16b .section-8 path,#mermaid-svg-DuyySu6dBz6qf16b .section-8 circle,#mermaid-svg-DuyySu6dBz6qf16b .section-8 polygon,#mermaid-svg-DuyySu6dBz6qf16b .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .section-8 text{fill:black;}#mermaid-svg-DuyySu6dBz6qf16b .node-icon-8{font-size:40px;color:black;}#mermaid-svg-DuyySu6dBz6qf16b .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .edge-depth-8{stroke-width:-10;}#mermaid-svg-DuyySu6dBz6qf16b .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-DuyySu6dBz6qf16b .disabled,#mermaid-svg-DuyySu6dBz6qf16b .disabled circle,#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:lightgray;}#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:#efefef;}#mermaid-svg-DuyySu6dBz6qf16b .section-9 rect,#mermaid-svg-DuyySu6dBz6qf16b .section-9 path,#mermaid-svg-DuyySu6dBz6qf16b .section-9 circle,#mermaid-svg-DuyySu6dBz6qf16b .section-9 polygon,#mermaid-svg-DuyySu6dBz6qf16b .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .section-9 text{fill:black;}#mermaid-svg-DuyySu6dBz6qf16b .node-icon-9{font-size:40px;color:black;}#mermaid-svg-DuyySu6dBz6qf16b .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .edge-depth-9{stroke-width:-13;}#mermaid-svg-DuyySu6dBz6qf16b .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-DuyySu6dBz6qf16b .disabled,#mermaid-svg-DuyySu6dBz6qf16b .disabled circle,#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:lightgray;}#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:#efefef;}#mermaid-svg-DuyySu6dBz6qf16b .section-10 rect,#mermaid-svg-DuyySu6dBz6qf16b .section-10 path,#mermaid-svg-DuyySu6dBz6qf16b .section-10 circle,#mermaid-svg-DuyySu6dBz6qf16b .section-10 polygon,#mermaid-svg-DuyySu6dBz6qf16b .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .section-10 text{fill:black;}#mermaid-svg-DuyySu6dBz6qf16b .node-icon-10{font-size:40px;color:black;}#mermaid-svg-DuyySu6dBz6qf16b .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .edge-depth-10{stroke-width:-16;}#mermaid-svg-DuyySu6dBz6qf16b .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-DuyySu6dBz6qf16b .disabled,#mermaid-svg-DuyySu6dBz6qf16b .disabled circle,#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:lightgray;}#mermaid-svg-DuyySu6dBz6qf16b .disabled text{fill:#efefef;}#mermaid-svg-DuyySu6dBz6qf16b .section-root rect,#mermaid-svg-DuyySu6dBz6qf16b .section-root path,#mermaid-svg-DuyySu6dBz6qf16b .section-root circle,#mermaid-svg-DuyySu6dBz6qf16b .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-DuyySu6dBz6qf16b .section-root text{fill:#ffffff;}#mermaid-svg-DuyySu6dBz6qf16b .section-root span{color:#ffffff;}#mermaid-svg-DuyySu6dBz6qf16b .section-2 span{color:#ffffff;}#mermaid-svg-DuyySu6dBz6qf16b .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-DuyySu6dBz6qf16b .edge{fill:none;}#mermaid-svg-DuyySu6dBz6qf16b .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-DuyySu6dBz6qf16b :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Rust 调 C++ 的 4 座大山
名字修饰
C++ 符号被 mangling 成长串怪名
没有简单的朴素函数名可链接
虚函数机制
成员函数地址藏在 vtable 里
还要隐式传 this 指针
回调反向调用
不是我们调它是它调我们
要伪造一个 C++ 对象给它调
内存布局
结构体/枚举必须逐字节一致
对齐、pack、union、位域全是坑
本教程把四座大山一座座翻过去。每座山都遵循同一套路:先用 C++ 视角搞懂原理 → 再用 Rust 语法精确复刻 → 最后用测试锁死正确性。
0.3 方案选型:为什么是 libloading + 手工 vtable
| 方案 | 做法 | 优点 | 缺点 | 结论 |
|---|---|---|---|---|
| A. bindgen + .lib 静态链接 | bindgen 读头文件生成绑定,链接 xtptraderapi.lib |
类型自动生成 | bindgen 对 C++ 类/虚函数支持很差,生成的绑定基本不可用;还要处理 MSVC 链接环境 | ❌ |
| B. 写 C++ shim 再包 C 接口 | 用 C++ 写一层 extern "C" 包装,编译成新 DLL |
最稳妥 | 需要 MSVC 编译环境,多一层构建步骤,迭代慢 | 备选 |
| C. libloading + 手工 vtable | 运行时 LoadLibrary,读出对象 vtable,把虚函数当函数指针调 |
无需编译 C++,纯 Rust 可控,分发只需带原 DLL | 需要手工保证布局正确(本教程的核心技能) | ✅ 本文方案 |
提示:方案 C 要求 x64 Windows 。x64 下 MSVC 的调用约定与 Rust
extern "C"一致(下一章细讲),这是方案成立的前提。32 位下thiscall约定不同,方案 C 不成立。
第 1 章 认识 XTP API 的构造
1.1 两个 DLL、两套类
#mermaid-svg-nx3hpZeaHQzzFA7p{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nx3hpZeaHQzzFA7p .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nx3hpZeaHQzzFA7p .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nx3hpZeaHQzzFA7p .error-icon{fill:#552222;}#mermaid-svg-nx3hpZeaHQzzFA7p .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nx3hpZeaHQzzFA7p .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nx3hpZeaHQzzFA7p .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nx3hpZeaHQzzFA7p .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nx3hpZeaHQzzFA7p .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nx3hpZeaHQzzFA7p .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nx3hpZeaHQzzFA7p .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nx3hpZeaHQzzFA7p .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nx3hpZeaHQzzFA7p .marker.cross{stroke:#333333;}#mermaid-svg-nx3hpZeaHQzzFA7p svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nx3hpZeaHQzzFA7p p{margin:0;}#mermaid-svg-nx3hpZeaHQzzFA7p .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nx3hpZeaHQzzFA7p .cluster-label text{fill:#333;}#mermaid-svg-nx3hpZeaHQzzFA7p .cluster-label span{color:#333;}#mermaid-svg-nx3hpZeaHQzzFA7p .cluster-label span p{background-color:transparent;}#mermaid-svg-nx3hpZeaHQzzFA7p .label text,#mermaid-svg-nx3hpZeaHQzzFA7p span{fill:#333;color:#333;}#mermaid-svg-nx3hpZeaHQzzFA7p .node rect,#mermaid-svg-nx3hpZeaHQzzFA7p .node circle,#mermaid-svg-nx3hpZeaHQzzFA7p .node ellipse,#mermaid-svg-nx3hpZeaHQzzFA7p .node polygon,#mermaid-svg-nx3hpZeaHQzzFA7p .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nx3hpZeaHQzzFA7p .rough-node .label text,#mermaid-svg-nx3hpZeaHQzzFA7p .node .label text,#mermaid-svg-nx3hpZeaHQzzFA7p .image-shape .label,#mermaid-svg-nx3hpZeaHQzzFA7p .icon-shape .label{text-anchor:middle;}#mermaid-svg-nx3hpZeaHQzzFA7p .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nx3hpZeaHQzzFA7p .rough-node .label,#mermaid-svg-nx3hpZeaHQzzFA7p .node .label,#mermaid-svg-nx3hpZeaHQzzFA7p .image-shape .label,#mermaid-svg-nx3hpZeaHQzzFA7p .icon-shape .label{text-align:center;}#mermaid-svg-nx3hpZeaHQzzFA7p .node.clickable{cursor:pointer;}#mermaid-svg-nx3hpZeaHQzzFA7p .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nx3hpZeaHQzzFA7p .arrowheadPath{fill:#333333;}#mermaid-svg-nx3hpZeaHQzzFA7p .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nx3hpZeaHQzzFA7p .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nx3hpZeaHQzzFA7p .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nx3hpZeaHQzzFA7p .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nx3hpZeaHQzzFA7p .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nx3hpZeaHQzzFA7p .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nx3hpZeaHQzzFA7p .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nx3hpZeaHQzzFA7p .cluster text{fill:#333;}#mermaid-svg-nx3hpZeaHQzzFA7p .cluster span{color:#333;}#mermaid-svg-nx3hpZeaHQzzFA7p div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nx3hpZeaHQzzFA7p .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nx3hpZeaHQzzFA7p rect.text{fill:none;stroke-width:0;}#mermaid-svg-nx3hpZeaHQzzFA7p .icon-shape,#mermaid-svg-nx3hpZeaHQzzFA7p .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nx3hpZeaHQzzFA7p .icon-shape p,#mermaid-svg-nx3hpZeaHQzzFA7p .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nx3hpZeaHQzzFA7p .icon-shape .label rect,#mermaid-svg-nx3hpZeaHQzzFA7p .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nx3hpZeaHQzzFA7p .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nx3hpZeaHQzzFA7p .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nx3hpZeaHQzzFA7p :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} xtptraderapi.dll (v2.2.x 交易)
xtpxquoteapi.dll (XTPX 4.0 行情)
注册
注册
回调
回调
QuoteApi
36 个虚函数
Login/Subscribe/Query...
QuoteSpi
38 个虚函数
OnDepthMarketData/OnTickByTick...
TraderApi
80+ 个虚函数
Login/InsertOrder/QueryAsset...
TraderSpi
65 个虚函数
OnOrderEvent/OnTradeEvent...
用户代码
- Api 类 (我们调它):
QuoteApi/TraderApi。通过静态工厂函数CreateQuoteApi()/CreateTraderApi()创建,得到一个指向 C++ 对象的指针。 - Spi 类 (它调我们):
QuoteSpi/TraderSpi。我们"实现"它并用RegisterSpi()注册,之后 DLL 在内部线程里回调。
注意:行情 API 有两代。中泰同时维护着经典版 (
xtpquoteapi.dll,命名空间XTP::API,随交易包分发)和 XTPX 4.0 版 (xtpxquoteapi.dll,命名空间XTPX::API,独立行情包)。两代接口的函数名大多相同,但结构体布局、枚举值、部分接口都不同,混用必崩。本教程封装的是 XTPX 4.0 版,第 17 章专门讲两代的差异。
1.2 官方 C++ demo 的标准使用流程
从官方 demo(XTPApiDemo/src/xtp_api_demo.cpp)提炼出的骨架,我们封装的最终目标就是让 Rust 代码长得和它一一对应:
cpp
// ------ 行情 ------
XTP::API::QuoteApi* pQuoteApi = XTP::API::QuoteApi::CreateQuoteApi(client_id, filepath, XTP_LOG_LEVEL_DEBUG);
MyQuoteSpi* pQuoteSpi = new MyQuoteSpi();
pQuoteApi->RegisterSpi(pQuoteSpi);
pQuoteApi->SetHeartBeatInterval(hb);
int r = pQuoteApi->Login(ip, port, user, pass, XTP_PROTOCOL_TCP);
if (r == 0) {
pQuoteApi->SubscribeMarketData(tickers, count, XTP_EXCHANGE_SH);
// ... 回调开始到达:OnSubMarketData / OnDepthMarketData ...
}
pQuoteApi->Logout();
pQuoteApi->Release(); // 释放对象
// ------ 交易 ------
XTP::API::TraderApi* pTraderApi = XTP::API::TraderApi::CreateTraderApi(client_id, filepath, XTP_LOG_LEVEL_DEBUG);
pTraderApi->RegisterSpi(pTradeSpi);
pTraderApi->SubscribePublicTopic(XTP_TERT_QUICK);
pTraderApi->SetSoftwareVersion("1.0.0");
pTraderApi->SetSoftwareKey("YOUR_KEY");
uint64_t session = pTraderApi->Login(ip, port, user, pass, XTP_PROTOCOL_TCP);
if (session > 0) {
XTPOrderInsertInfo order = {0};
// ... 填单 ...
uint64_t xtp_id = pTraderApi->InsertOrder(&order, session);
}
pTraderApi->Release();
注意时序约束 :
RegisterSpi、SetHeartBeatInterval、SetSoftwareKey等都必须 在 Login 之前 调用;Release在最后。封装层的文档注释要把这些约束写清楚。
1.3 头文件即合同
XTP 的所有"合同条款"都在头文件里,封装就是逐条翻译成 Rust:
| 头文件 | 内容 | 我们的对应物 |
|---|---|---|
xtpx_quote_api.h |
QuoteApi/QuoteSpi 类声明(虚函数顺序) | vtable 结构体(第 4 章) |
xquote_x_api_struct.h |
行情结构体(注意 #pragma pack(1)) |
quote_types.rs |
xquote_x_api_data_type.h |
行情枚举/常量(多为 typedef uint32_t + constexpr) |
quote_types.rs 枚举 |
xtp_trader_api.h |
TraderApi/TraderSpi 类声明 | vtable 结构体 |
xoms_api_struct.h |
交易结构体(#pragma pack(8)) |
trader_types.rs |
xtp_api_data_type.h |
交易枚举/常量/字符串长度宏 | trader_types.rs/common.rs |
xtp_api_struct_common.h |
XTPRspInfoStruct(错误信息) |
common.rs::XtpRspInfo |
铁律 :任何字段、枚举值、槽位顺序,只信头文件,不信记忆、不信别的语言的封装、不信文档站的二手描述。
第 2 章 补课:ABI、调用约定与 vtable
这一章是全教程的理论地基,小白务必看懂。
2.1 ABI 是什么
源代码层面的"函数签名"在编译后并不存在。两个编译产物要互相调用,必须约定:
- 参数放在哪里(寄存器?栈?顺序?)
- 返回值放在哪里
- 谁清理栈
- 结构体在内存里怎么排布(对齐)
- 符号叫什么名字(名字修饰)
这套约定叫 ABI (Application Binary Interface)。Rust 和 C++ 语言不同,但只要 遵守同一份 ABI,就能互相调用。
2.2 x64 Windows 调用约定(我们唯一的依靠)
Windows x64 只有一种调用约定(有时叫 Microsoft x64 calling convention):
#mermaid-svg-OBmIjHxtoABLdOr9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OBmIjHxtoABLdOr9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OBmIjHxtoABLdOr9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OBmIjHxtoABLdOr9 .error-icon{fill:#552222;}#mermaid-svg-OBmIjHxtoABLdOr9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OBmIjHxtoABLdOr9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OBmIjHxtoABLdOr9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OBmIjHxtoABLdOr9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OBmIjHxtoABLdOr9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OBmIjHxtoABLdOr9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OBmIjHxtoABLdOr9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OBmIjHxtoABLdOr9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OBmIjHxtoABLdOr9 .marker.cross{stroke:#333333;}#mermaid-svg-OBmIjHxtoABLdOr9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OBmIjHxtoABLdOr9 p{margin:0;}#mermaid-svg-OBmIjHxtoABLdOr9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OBmIjHxtoABLdOr9 .cluster-label text{fill:#333;}#mermaid-svg-OBmIjHxtoABLdOr9 .cluster-label span{color:#333;}#mermaid-svg-OBmIjHxtoABLdOr9 .cluster-label span p{background-color:transparent;}#mermaid-svg-OBmIjHxtoABLdOr9 .label text,#mermaid-svg-OBmIjHxtoABLdOr9 span{fill:#333;color:#333;}#mermaid-svg-OBmIjHxtoABLdOr9 .node rect,#mermaid-svg-OBmIjHxtoABLdOr9 .node circle,#mermaid-svg-OBmIjHxtoABLdOr9 .node ellipse,#mermaid-svg-OBmIjHxtoABLdOr9 .node polygon,#mermaid-svg-OBmIjHxtoABLdOr9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OBmIjHxtoABLdOr9 .rough-node .label text,#mermaid-svg-OBmIjHxtoABLdOr9 .node .label text,#mermaid-svg-OBmIjHxtoABLdOr9 .image-shape .label,#mermaid-svg-OBmIjHxtoABLdOr9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-OBmIjHxtoABLdOr9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OBmIjHxtoABLdOr9 .rough-node .label,#mermaid-svg-OBmIjHxtoABLdOr9 .node .label,#mermaid-svg-OBmIjHxtoABLdOr9 .image-shape .label,#mermaid-svg-OBmIjHxtoABLdOr9 .icon-shape .label{text-align:center;}#mermaid-svg-OBmIjHxtoABLdOr9 .node.clickable{cursor:pointer;}#mermaid-svg-OBmIjHxtoABLdOr9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OBmIjHxtoABLdOr9 .arrowheadPath{fill:#333333;}#mermaid-svg-OBmIjHxtoABLdOr9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OBmIjHxtoABLdOr9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OBmIjHxtoABLdOr9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OBmIjHxtoABLdOr9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OBmIjHxtoABLdOr9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OBmIjHxtoABLdOr9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OBmIjHxtoABLdOr9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OBmIjHxtoABLdOr9 .cluster text{fill:#333;}#mermaid-svg-OBmIjHxtoABLdOr9 .cluster span{color:#333;}#mermaid-svg-OBmIjHxtoABLdOr9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OBmIjHxtoABLdOr9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OBmIjHxtoABLdOr9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-OBmIjHxtoABLdOr9 .icon-shape,#mermaid-svg-OBmIjHxtoABLdOr9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OBmIjHxtoABLdOr9 .icon-shape p,#mermaid-svg-OBmIjHxtoABLdOr9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OBmIjHxtoABLdOr9 .icon-shape .label rect,#mermaid-svg-OBmIjHxtoABLdOr9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OBmIjHxtoABLdOr9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OBmIjHxtoABLdOr9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OBmIjHxtoABLdOr9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 返回值
整数 / 指针 → RAX
浮点 → XMM0
浮点参数:独立通道
① XMM0
② XMM1
③ XMM2
④ XMM3
整数 / 指针参数:从左到右依次放入寄存器
① RCX
② RDX
③ R8
④ R9
⑤ 起:栈上
要点:
-
Rust 的
extern "C" fn在 x86_64-pc-windows-msvc 目标上就是这套约定; -
C++ 非静态成员函数 会把
this当作第一个隐藏参数传入 RCX。所以:cppobj->Login(ip, port) // C++ 写法 // 等价于 C 写法: TraderApi_Login(obj, ip, port) // this 成了第一个参数 -
成员函数与
extern "C" fn(this: *mut c_void, ...)参数逐一对齐后,可以直接互调; -
bool是 1 字节,C++bool与 Rustbool兼容; -
超过 8 字节的结构体按值返回时有特殊规则(隐藏返回参数)------XTP 没有这种函数,虚函数全部返回指针/整数/bool,所以不用管。
2.3 虚函数与 vtable:C++ 多态的内存真相
C++ 对象如果有虚函数,它的前 8 字节是一个指向"虚函数表"(vtable)的指针:
#mermaid-svg-ig6Di8kRj6bKsJth{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ig6Di8kRj6bKsJth .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ig6Di8kRj6bKsJth .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ig6Di8kRj6bKsJth .error-icon{fill:#552222;}#mermaid-svg-ig6Di8kRj6bKsJth .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ig6Di8kRj6bKsJth .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ig6Di8kRj6bKsJth .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ig6Di8kRj6bKsJth .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ig6Di8kRj6bKsJth .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ig6Di8kRj6bKsJth .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ig6Di8kRj6bKsJth .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ig6Di8kRj6bKsJth .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ig6Di8kRj6bKsJth .marker.cross{stroke:#333333;}#mermaid-svg-ig6Di8kRj6bKsJth svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ig6Di8kRj6bKsJth p{margin:0;}#mermaid-svg-ig6Di8kRj6bKsJth .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ig6Di8kRj6bKsJth .cluster-label text{fill:#333;}#mermaid-svg-ig6Di8kRj6bKsJth .cluster-label span{color:#333;}#mermaid-svg-ig6Di8kRj6bKsJth .cluster-label span p{background-color:transparent;}#mermaid-svg-ig6Di8kRj6bKsJth .label text,#mermaid-svg-ig6Di8kRj6bKsJth span{fill:#333;color:#333;}#mermaid-svg-ig6Di8kRj6bKsJth .node rect,#mermaid-svg-ig6Di8kRj6bKsJth .node circle,#mermaid-svg-ig6Di8kRj6bKsJth .node ellipse,#mermaid-svg-ig6Di8kRj6bKsJth .node polygon,#mermaid-svg-ig6Di8kRj6bKsJth .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ig6Di8kRj6bKsJth .rough-node .label text,#mermaid-svg-ig6Di8kRj6bKsJth .node .label text,#mermaid-svg-ig6Di8kRj6bKsJth .image-shape .label,#mermaid-svg-ig6Di8kRj6bKsJth .icon-shape .label{text-anchor:middle;}#mermaid-svg-ig6Di8kRj6bKsJth .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ig6Di8kRj6bKsJth .rough-node .label,#mermaid-svg-ig6Di8kRj6bKsJth .node .label,#mermaid-svg-ig6Di8kRj6bKsJth .image-shape .label,#mermaid-svg-ig6Di8kRj6bKsJth .icon-shape .label{text-align:center;}#mermaid-svg-ig6Di8kRj6bKsJth .node.clickable{cursor:pointer;}#mermaid-svg-ig6Di8kRj6bKsJth .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ig6Di8kRj6bKsJth .arrowheadPath{fill:#333333;}#mermaid-svg-ig6Di8kRj6bKsJth .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ig6Di8kRj6bKsJth .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ig6Di8kRj6bKsJth .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ig6Di8kRj6bKsJth .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ig6Di8kRj6bKsJth .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ig6Di8kRj6bKsJth .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ig6Di8kRj6bKsJth .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ig6Di8kRj6bKsJth .cluster text{fill:#333;}#mermaid-svg-ig6Di8kRj6bKsJth .cluster span{color:#333;}#mermaid-svg-ig6Di8kRj6bKsJth div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ig6Di8kRj6bKsJth .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ig6Di8kRj6bKsJth rect.text{fill:none;stroke-width:0;}#mermaid-svg-ig6Di8kRj6bKsJth .icon-shape,#mermaid-svg-ig6Di8kRj6bKsJth .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ig6Di8kRj6bKsJth .icon-shape p,#mermaid-svg-ig6Di8kRj6bKsJth .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ig6Di8kRj6bKsJth .icon-shape .label rect,#mermaid-svg-ig6Di8kRj6bKsJth .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ig6Di8kRj6bKsJth .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ig6Di8kRj6bKsJth .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ig6Di8kRj6bKsJth :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ③ 调用过程(以 GetApiVersion 为例)
② vtable:DLL 中的只读函数指针数组
① C++ 对象内存
指向
取槽位 1
vtable 指针
对象的前 8 字节
成员数据 ...
槽位 0 → Release
槽位 1 → GetApiVersion
槽位 2 → GetApiLastError
槽位 3 → RegisterSpi
槽位 4 → SetHeartBeatInterval
... 按头文件声明顺序排 ...
取 vtable1
得到函数指针
把 obj 作为 this
传入并调用
调用 obj->GetApiVersion() 时,编译器生成的逻辑是:
1. vtable = *(void***)obj; // 取对象前 8 字节
2. fn = vtable[1]; // GetApiVersion 是第 2 个虚函数(槽位 1)
3. fn(obj); // 把 obj 当 this 传进去调
槽位顺序 = 头文件中虚函数的声明顺序 (单继承、无虚析构的情况下)。XTP 的两个 Api 类析构函数都不是 virtual,所以第一个虚函数(Release)就在槽位 0,没有偏移。
我们只要:
- 读出对象前 8 字节得到 vtable 地址;
- 把 vtable 解释成一个"函数指针数组";
- 用对应槽位的函数指针,以
this为第一参数调用------
就等于在 Rust 里调用了 C++ 虚函数。这就是第 4 章的全部秘密。
2.4 MSVC 名字修饰(name mangling)
C++ 支持重载,所以编译器把 CreateQuoteApi(uint8_t, const char*, XTP_LOG_LEVEL, bool) 这类信息编码进符号名:
?CreateQuoteApi@QuoteApi@API@XTPX@@SAPEAV123@EPEBDW4XTP_LOG_LEVEL@23@_N@Z
我们不用读懂全部,只要会用工具查:
powershell
dumpbin /exports xtpxquoteapi.dll | findstr Create
# 6 5 000135C0 ?CreateQuoteApi@QuoteApi@API@XTPX@@SAPEAV123@EPEBDW4XTP_LOG_LEVEL@23@_N@Z
dumpbin /exports xtptraderapi.dll | findstr Create
# 6 5 0000FE20 ?CreateTraderApi@TraderApi@API@XTP@@SAPEAV123@EPEBDW4XTP_LOG_LEVEL@@@Z
dumpbin 随 Visual Studio 附带(在开始菜单 "x64 Native Tools Command Prompt" 里可用)。
工程建议 :代码里先试修饰名、再试朴素名
"CreateQuoteApi"兜底(某些平台导出未修饰名)。验证习惯 :修饰名写错一个字符
library.get()就会失败,把实际导出名复制进代码注释里备查。
2.5 对齐、padding 与 #pragma pack
CPU 读 8 字节的 double 时,要求地址是 8 的倍数(对齐)。编译器在结构体字段间自动插入填充(padding):
#mermaid-svg-gSQI7wWsVve1Bzsq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gSQI7wWsVve1Bzsq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gSQI7wWsVve1Bzsq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gSQI7wWsVve1Bzsq .error-icon{fill:#552222;}#mermaid-svg-gSQI7wWsVve1Bzsq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gSQI7wWsVve1Bzsq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gSQI7wWsVve1Bzsq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gSQI7wWsVve1Bzsq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gSQI7wWsVve1Bzsq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gSQI7wWsVve1Bzsq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gSQI7wWsVve1Bzsq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gSQI7wWsVve1Bzsq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gSQI7wWsVve1Bzsq .marker.cross{stroke:#333333;}#mermaid-svg-gSQI7wWsVve1Bzsq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gSQI7wWsVve1Bzsq p{margin:0;}#mermaid-svg-gSQI7wWsVve1Bzsq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gSQI7wWsVve1Bzsq .cluster-label text{fill:#333;}#mermaid-svg-gSQI7wWsVve1Bzsq .cluster-label span{color:#333;}#mermaid-svg-gSQI7wWsVve1Bzsq .cluster-label span p{background-color:transparent;}#mermaid-svg-gSQI7wWsVve1Bzsq .label text,#mermaid-svg-gSQI7wWsVve1Bzsq span{fill:#333;color:#333;}#mermaid-svg-gSQI7wWsVve1Bzsq .node rect,#mermaid-svg-gSQI7wWsVve1Bzsq .node circle,#mermaid-svg-gSQI7wWsVve1Bzsq .node ellipse,#mermaid-svg-gSQI7wWsVve1Bzsq .node polygon,#mermaid-svg-gSQI7wWsVve1Bzsq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gSQI7wWsVve1Bzsq .rough-node .label text,#mermaid-svg-gSQI7wWsVve1Bzsq .node .label text,#mermaid-svg-gSQI7wWsVve1Bzsq .image-shape .label,#mermaid-svg-gSQI7wWsVve1Bzsq .icon-shape .label{text-anchor:middle;}#mermaid-svg-gSQI7wWsVve1Bzsq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gSQI7wWsVve1Bzsq .rough-node .label,#mermaid-svg-gSQI7wWsVve1Bzsq .node .label,#mermaid-svg-gSQI7wWsVve1Bzsq .image-shape .label,#mermaid-svg-gSQI7wWsVve1Bzsq .icon-shape .label{text-align:center;}#mermaid-svg-gSQI7wWsVve1Bzsq .node.clickable{cursor:pointer;}#mermaid-svg-gSQI7wWsVve1Bzsq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gSQI7wWsVve1Bzsq .arrowheadPath{fill:#333333;}#mermaid-svg-gSQI7wWsVve1Bzsq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gSQI7wWsVve1Bzsq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gSQI7wWsVve1Bzsq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gSQI7wWsVve1Bzsq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gSQI7wWsVve1Bzsq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gSQI7wWsVve1Bzsq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gSQI7wWsVve1Bzsq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gSQI7wWsVve1Bzsq .cluster text{fill:#333;}#mermaid-svg-gSQI7wWsVve1Bzsq .cluster span{color:#333;}#mermaid-svg-gSQI7wWsVve1Bzsq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gSQI7wWsVve1Bzsq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gSQI7wWsVve1Bzsq rect.text{fill:none;stroke-width:0;}#mermaid-svg-gSQI7wWsVve1Bzsq .icon-shape,#mermaid-svg-gSQI7wWsVve1Bzsq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gSQI7wWsVve1Bzsq .icon-shape p,#mermaid-svg-gSQI7wWsVve1Bzsq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gSQI7wWsVve1Bzsq .icon-shape .label rect,#mermaid-svg-gSQI7wWsVve1Bzsq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gSQI7wWsVve1Bzsq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gSQI7wWsVve1Bzsq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gSQI7wWsVve1Bzsq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} #pragma pack(1)
0-4: u32 a
4-12: f64 b
默认对齐(align 8)
0-4: u32 a
4-8: (填充4字节)
8-16: f64 b
- 交易头文件用
#pragma pack(8)⇒ 自然对齐 ,与 Rust#[repr(C)]完全一致; - 行情头文件用
#pragma pack(1)⇒ 紧凑排列 ,理论上要对应用#[repr(C, packed)]。
但实测发现:行情结构体因为字段排列的巧合(char16 开头 + 成对的 4 字节字段),自然对齐结果与 pack(1) 完全相同 ------唯独 XtpNqFullInfo 的 6 个单字节位域翻了车(第 5 章详讲)。这种巧合不能靠猜,必须逐个结构体手工计算验证。
第 3 章 工程搭建与 DLL 加载
3.1 工程骨架
xtp-api/
├── Cargo.toml
├── build.rs # 构建期:找 SDK、拷贝 DLL
├── src/
│ ├── lib.rs # 模块声明
│ ├── common.rs # 公共枚举、错误类型、XtpRspInfo
│ ├── quote_ffi.rs # 行情:vtable 定义 + 裸调用
│ ├── quote_types.rs # 行情:结构体/枚举
│ ├── quote.rs # 行情:安全封装 + SPI 桥
│ ├── trader_ffi.rs # 交易:同上
│ ├── trader_types.rs
│ └── trader.rs
├── tests/integration.rs
└── examples/
toml
# Cargo.toml
[package]
name = "xtp-api"
version = "0.2.0"
edition = "2021"
[dependencies]
libloading = "0.8" # 运行时加载 DLL
thiserror = "2" # 错误类型 derive
[dev-dependencies]
env_logger = "0.11"
3.2 用 libloading 加载 DLL 并找到工厂函数
rust
use libloading::{Library, Symbol};
use std::ffi::{c_char, c_void};
const QUOTE_DLL_NAME: &str = "xtpxquoteapi.dll";
// CreateQuoteApi(uint8_t, const char*, XTP_LOG_LEVEL, bool) -> QuoteApi*
type CreateQuoteApiFn = unsafe extern "C" fn(
client_id: u8,
save_file_path: *const c_char,
log_level: i32,
udpseq_output: bool,
) -> *mut c_void;
let library = unsafe { Library::new(QUOTE_DLL_NAME) }
.expect("加载 DLL 失败,确认 xtpxquoteapi.dll 在 exe 同目录或 PATH 中");
// 先试 MSVC 修饰名,再试朴素名兜底
let create_fn: Symbol<CreateQuoteApiFn> = {
let names = [
"?CreateQuoteApi@QuoteApi@API@XTPX@@SAPEAV123@EPEBDW4XTP_LOG_LEVEL@23@_N@Z",
"CreateQuoteApi",
];
let mut found = None;
for name in &names {
if let Ok(sym) = unsafe { library.get::<CreateQuoteApiFn>(name.as_bytes()) } {
found = Some(sym);
break;
}
}
found.expect("两个名字都找不到,用 dumpbin /exports 核对真实导出名")
};
Library::new("xtpxquoteapi.dll") 的搜索路径与 Windows LoadLibrary 一致:exe 所在目录 → 系统目录 → 当前目录 → PATH。开发期最省心的做法是把 DLL 放在 crate 根目录(cargo run/test 的工作目录),或写 build.rs 自动拷贝(见 3.4)。
小坑 :
Library::new(path)在 libloading 0.8 中是unsafe(官方理由:加载的库可能运行任意初始化代码)。包一层错误处理即可。
3.3 创建 C++ 对象
rust
let save_path = std::ffi::CString::new("./data").unwrap();
let obj = unsafe { create_fn(1, save_path.as_ptr(), 4 /*DEBUG*/, true) };
assert!(!obj.is_null(), "CreateQuoteApi 返回空指针");
此刻 obj 就是一个活生生的 C++ 对象,它的前 8 字节就是 vtable 指针。
3.4 build.rs:自动拷贝 DLL
rust
// build.rs(核心逻辑)
use std::{env, fs, path::PathBuf};
fn main() {
println!("cargo:rerun-if-env-changed=XTP_SDK_PATH");
if let Ok(root) = env::var("XTP_SDK_PATH") {
let root = PathBuf::from(root);
let dst = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
for (dir, dll) in [
(find_sub(&root, "XTP_API_", "bin/win64/dll"), "xtptraderapi.dll"),
(find_sub(&root, "XTPXQuoteAPI_", "lib/win64"), "xtpxquoteapi.dll"),
] {
if let Some(d) = dir {
let src = d.join(dll);
if src.exists() {
let _ = fs::copy(&src, dst.join(dll));
}
}
}
} else {
println!("cargo:warning=未设置 XTP_SDK_PATH,请手动把 DLL 放到 crate 根目录");
}
}
反面教材 (真实踩坑):第一版 build.rs 把
C:\Users\xxx\Desktop\xtp这种个人绝对路径 和XTP_API_20250806_2.2.50.8这种带版本号的目录名 硬编码进去,换台机器就废。规则:只用环境变量,且拷贝前校验目标 DLL 真实存在。
第 4 章 重建 vtable:调用第一个 C++ 虚函数
4.1 把 vtable 声明成一个 Rust 结构体
对着 xtpx_quote_api.h 里 QuoteApi 的虚函数声明顺序 ,写一个 #[repr(C)] 结构体,每个字段是一个函数指针:
rust
use std::ffi::{c_char, c_int, c_void};
#[repr(C)]
pub struct QuoteApiVTable {
pub release: unsafe extern "C" fn(this: *mut c_void),
pub get_api_version: unsafe extern "C" fn(this: *mut c_void) -> *const c_char,
pub get_api_last_error: unsafe extern "C" fn(this: *mut c_void) -> *mut XtpRspInfo,
pub register_spi: unsafe extern "C" fn(this: *mut c_void, spi: *mut c_void),
pub set_heart_beat_interval: unsafe extern "C" fn(this: *mut c_void, interval: u32),
pub set_config_file: unsafe extern "C" fn(this: *mut c_void, filename: *const c_char) -> bool,
pub set_udp_thread_affinity: unsafe extern "C" fn(this: *mut c_void, cpu: *mut i32, count: i32) -> bool,
pub subscribe_market_data: unsafe extern "C" fn(this: *mut c_void, ticker: *mut *mut c_char, count: c_int, ex: u32) -> c_int,
// ... 36 个槽位,与头文件逐行对应,一个不能多、一个不能少、顺序不能乱
}
检查清单(抄 vtable 时对着勾):
- 头文件里每个
virtual xxx = 0;都有对应字段; - 顺序与声明顺序完全一致(纯虚/带默认实现不影响顺序);
- 析构函数不是 virtual ⇒ 不占槽位(若是 virtual,会占槽位,顺序还要算它);
- 参数类型逐个翻译(见下面映射表),返回值类型别漏;
- 默认参数(如
const char* local_ip = NULL)不影响签名,照写。
4.2 参数类型映射速查
| C++ 类型 | Rust 类型 | 说明 |
|---|---|---|
int / 普通 enum |
i32 (参数也可用 c_int) |
enum 默认 int |
uint32_t / typedef uint32_t X |
u32 |
行情的 XTP_EXCHANGE_TYPE 就是这种 |
uint64_t |
u64 |
session_id、order_xtp_id |
int64_t |
i64 |
数量、时间 |
double |
f64 |
价格、金额 |
bool |
bool |
同为 1 字节 |
uint8_t |
u8 |
client_id |
int16_t |
i16 |
channel_number |
char |
u8(字符串数组用 [u8; N]) |
C 的 char 有无符号平台差异,FFI 里按字节处理最稳 |
const char* |
*const c_char |
入参字符串 |
char* / char*[] |
*mut c_char / *mut *mut c_char |
订阅 ticker 数组 |
T* (结构体指针) |
*mut T / *const T |
视 const 性 |
void 返回 |
无返回类型 |
4.3 读 vtable 指针并调用
rust
pub struct RawQuoteApi {
_library: std::sync::Arc<Library>, // 保证 DLL 一直加载(见第 8 章)
vtable: *const QuoteApiVTable, // 指向 DLL 内部的 vtable
obj: *mut c_void,
}
impl RawQuoteApi {
fn vt(&self) -> &QuoteApiVTable {
unsafe { &*self.vtable } // vtable 随 DLL 常驻,生命周期由 _library 保证
}
}
// 构造时(接 3.3 的 obj):
let vtable = unsafe { *(obj as *const *const QuoteApiVTable) };
assert!(!vtable.is_null());
// 调用 GetApiVersion:
let version = unsafe {
let p = (api.vt().get_api_version)(api.obj);
if p.is_null() { String::new() } else { std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned() }
};
println!("{}", version); // => 1.2.1-r.3 🎉
GetApiVersion() vtable C++ 对象 Rust 代码 GetApiVersion() vtable C++ 对象 Rust 代码 #mermaid-svg-hirFiueTvTJ6gzMV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hirFiueTvTJ6gzMV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hirFiueTvTJ6gzMV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hirFiueTvTJ6gzMV .error-icon{fill:#552222;}#mermaid-svg-hirFiueTvTJ6gzMV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hirFiueTvTJ6gzMV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hirFiueTvTJ6gzMV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hirFiueTvTJ6gzMV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hirFiueTvTJ6gzMV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hirFiueTvTJ6gzMV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hirFiueTvTJ6gzMV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hirFiueTvTJ6gzMV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hirFiueTvTJ6gzMV .marker.cross{stroke:#333333;}#mermaid-svg-hirFiueTvTJ6gzMV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hirFiueTvTJ6gzMV p{margin:0;}#mermaid-svg-hirFiueTvTJ6gzMV .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hirFiueTvTJ6gzMV text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-hirFiueTvTJ6gzMV .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-hirFiueTvTJ6gzMV .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-hirFiueTvTJ6gzMV .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-hirFiueTvTJ6gzMV .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-hirFiueTvTJ6gzMV #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-hirFiueTvTJ6gzMV .sequenceNumber{fill:white;}#mermaid-svg-hirFiueTvTJ6gzMV #sequencenumber{fill:#333;}#mermaid-svg-hirFiueTvTJ6gzMV #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-hirFiueTvTJ6gzMV .messageText{fill:#333;stroke:none;}#mermaid-svg-hirFiueTvTJ6gzMV .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hirFiueTvTJ6gzMV .labelText,#mermaid-svg-hirFiueTvTJ6gzMV .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-hirFiueTvTJ6gzMV .loopText,#mermaid-svg-hirFiueTvTJ6gzMV .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-hirFiueTvTJ6gzMV .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-hirFiueTvTJ6gzMV .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-hirFiueTvTJ6gzMV .noteText,#mermaid-svg-hirFiueTvTJ6gzMV .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-hirFiueTvTJ6gzMV .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hirFiueTvTJ6gzMV .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hirFiueTvTJ6gzMV .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hirFiueTvTJ6gzMV .actorPopupMenu{position:absolute;}#mermaid-svg-hirFiueTvTJ6gzMV .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-hirFiueTvTJ6gzMV .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hirFiueTvTJ6gzMV .actor-man circle,#mermaid-svg-hirFiueTvTJ6gzMV line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-hirFiueTvTJ6gzMV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 读前 8 字节 vtable 地址 取槽位 1 函数指针 调用(this=obj) "1.2.1-r.3"
能打印出版本号,就证明方案 C 全链路成立。 剩下的所有虚函数都是同一姿势,纯体力活。
4.4 返回值与错误处理
XTP 的约定:
- 行情接口:返回
0成功,非 0 失败,失败原因用GetApiLastError()拿(XTPRI*); - 交易登录:返回
session_id,> 0成功,0失败; - 下单/资金划拨:返回 id,
0失败。
封装一个统一的取错函数:
rust
fn err(&self, api: &str, ret: i32) -> XtpError {
let ptr = unsafe { (self.vt().get_api_last_error)(self.obj) };
// 注意:GetApiLastError 也可能返回空指针,必须兜底
let rsp = unsafe { ptr.as_ref() }.copied().unwrap_or(XtpRspInfo::OK);
if rsp.error_id != 0 { XtpError::XtpRspError(rsp) }
else { XtpError::ApiError(format!("{} returned {}", api, ret)) }
}
真实案例 :登录失败时能正确拿到
XTP error 10200000: Login to quote server failed: the quote authentication server offline or no connection.------错误传播链就这么验证。
第 5 章 结构体逐字段映射(含对齐/pack/union/位域)
这是最容易出错、出错后最隐蔽的部分。一个字段错位,读出来的就是垃圾数据,而且不报错。
5.1 翻译规则
cpp
// C++
struct XTPTickerPriceInfo {
char ticker[XTP_QUOTE_TICKER_LEN]; // 16
XTP_EXCHANGE_TYPE exchange_id; // uint32_t
int32_t unused;
double last_price;
};
rust
// Rust ------ 一一对应
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct XtpTickerPriceInfo {
pub ticker: [u8; 16], // char[16] → [u8;16],不是 String!
pub exchange_id: u32,
pub unused: i32,
pub last_price: f64,
}
规则:
#[repr(C)]必须加,否则 Rust 自由重排字段;char[N]→[u8; N],不要 用String/CString(它们是指针,布局完全不同);- 定长数组、嵌套结构体、union 全部按同样规则递归翻译;
- 字符串读取另写辅助函数,字段本身保持字节数组。
rust
/// C 字符串(以 NUL 结尾) → String
pub fn from_cstring_bytes(buf: &[u8]) -> String {
std::ffi::CStr::from_bytes_until_nul(buf)
.map(|c| c.to_string_lossy().into_owned())
.unwrap_or_default()
}
impl XtpTickerPriceInfo {
pub fn ticker_str(&self) -> String { from_cstring_bytes(&self.ticker) }
}
5.2 手工计算布局:一个完整演练
以 XTPOrderInfo 为例(交易头文件,#pragma pack(8) 即自然对齐)。每个封装者都应该会这张表的手工推导:
| 字段 | 类型 | 大小 | 对齐 | 偏移 | 备注 |
|---|---|---|---|---|---|
| order_xtp_id | u64 | 8 | 8 | 0 | |
| order_client_id | u32 | 4 | 4 | 8 | |
| order_cancel_client_id | u32 | 4 | 4 | 12 | |
| order_cancel_xtp_id | u64 | 8 | 8 | 16 | |
| ticker | u8;16 | 16 | 1 | 24 | |
| market | i32 | 4 | 4 | 40 | |
| (填充) | - | 4 | - | 44 | f64 需 8 对齐 |
| price | f64 | 8 | 8 | 48 | |
| quantity | i64 | 8 | 8 | 56 | |
| price_type | i32 | 4 | 4 | 64 | |
| side / position_effect / reserved1 / reserved2 | u8×4 | 4 | 1 | 68 | C++ 是 union{u32; 4×u8} |
| business_type | i32 | 4 | 4 | 72 | |
| (填充) | - | 4 | - | 76 | |
| qty_traded | i64 | 8 | 8 | 80 | |
| qty_left | i64 | 8 | 8 | 88 | |
| insert_time | i64 | 8 | 8 | 96 | |
| update_time | i64 | 8 | 8 | 104 | |
| cancel_time | i64 | 8 | 8 | 112 | |
| trade_amount | f64 | 8 | 8 | 120 | |
| order_local_id | u8;11 | 11 | 1 | 128 | |
| (填充) | - | 1 | - | 139 | |
| order_status | i32 | 4 | 4 | 140 | |
| order_submit_status | i32 | 4 | 4 | 144 | |
| order_type | u8 | 1 | 1 | 148 | |
| (尾部填充) | - | 3 | - | 149 | 结构体对齐到 8 |
| 合计 | 152 |
rust
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct XtpOrderInfo {
pub order_xtp_id: u64,
pub order_client_id: u32,
pub order_cancel_client_id: u32,
pub order_cancel_xtp_id: u64,
pub ticker: [u8; 16],
pub market: i32,
pub price: f64,
pub quantity: i64,
pub price_type: i32,
pub side: u8,
pub position_effect: u8,
pub reserved1: u8, // union 里的预留字节,照 C++ 写
pub reserved2: u8,
pub business_type: i32,
pub qty_traded: i64,
pub qty_left: i64,
pub insert_time: i64,
pub update_time: i64,
pub cancel_time: i64,
pub trade_amount: f64,
pub order_local_id: [u8; 11],
pub order_status: i32,
pub order_submit_status: i32,
pub order_type: u8,
// C++ 到此为止(152 字节)。第一版在这里多加了 reserved: [u8; 7]
// 变成 160 字节 ------ 一个真实 bug。
}
心得 :Rust
repr(C)的对齐规则与 C 编译器相同,所以不需要 手写填充字段,Rust 会自动在同位置填充。你要保证的只是"字段序列与 C++ 相同"。显式写出reserved1/reserved2是因为 C++ 的 union 里真的命名了它们。
5.3 union 怎么翻译
C++ union → Rust union,同样 #[repr(C)],大小 = 最大成员,对齐 = 最大对齐:
cpp
union {
XTPMarketDataStockExData stk; // 224 字节
XTPMarketDataOptionExData opt; // 24 字节
XTPMarketDataBondExData bond; // 208 字节
};
rust
#[repr(C)]
#[derive(Copy, Clone)]
pub union MarketDataExUnion {
pub stk: MarketDataStockExData,
pub opt: MarketDataOptionExData,
pub bond: MarketDataBondExData,
}
// 大小 224。访问 union 字段是 unsafe:
// 先根据 data_type_v2 判断是哪个成员,再 unsafe { md.ex_data.stk.total_bid_qty }
配套的判别字段(data_type_v2)在结构体里:
rust
pub fn stock_ex(&self) -> Option<&MarketDataStockExData> {
match self.data_type_v2 {
2 /* ACTUAL */ => Some(unsafe { &self.ex_data.stk }),
_ => None,
}
}
5.4 #pragma pack(1) 与位域:最大的坑
行情头文件是 #pragma pack(1)。我们说过大部分结构体"巧合地"与自然对齐一致------但 XTPQuoteNQFullInfo 里有这么一段:
cpp
XTP_TRADE_STATUS trade_status : 8; // uint8_t 位域,占 1 字节
XTP_SECURITY_LEVEL security_level : 8; // 1 字节
XTP_TRADE_TYPE trade_type : 8; // 1 字节
XTP_SUSPEND_FLAG suspend_flag : 8; // 1 字节
XTP_EX_DIVIDEND_FLAG ex_dividend_flag : 8;// 1 字节
XTP_SECURITY_LAYER_TYPE layer_type : 8; // 1 字节
char reserved1[2];
char industry_type[6];
6 个位域共 6 字节 。第一版封装写成了 bitfields: u32(4 字节),于是 reserved1 之后所有字段全部向前错 2 字节 ------读 industry_type 读到的是 reserved1 的内容。
#mermaid-svg-IowZ15YRzCR0TnhM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IowZ15YRzCR0TnhM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IowZ15YRzCR0TnhM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IowZ15YRzCR0TnhM .error-icon{fill:#552222;}#mermaid-svg-IowZ15YRzCR0TnhM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IowZ15YRzCR0TnhM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IowZ15YRzCR0TnhM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IowZ15YRzCR0TnhM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IowZ15YRzCR0TnhM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IowZ15YRzCR0TnhM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IowZ15YRzCR0TnhM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IowZ15YRzCR0TnhM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IowZ15YRzCR0TnhM .marker.cross{stroke:#333333;}#mermaid-svg-IowZ15YRzCR0TnhM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IowZ15YRzCR0TnhM p{margin:0;}#mermaid-svg-IowZ15YRzCR0TnhM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IowZ15YRzCR0TnhM .cluster-label text{fill:#333;}#mermaid-svg-IowZ15YRzCR0TnhM .cluster-label span{color:#333;}#mermaid-svg-IowZ15YRzCR0TnhM .cluster-label span p{background-color:transparent;}#mermaid-svg-IowZ15YRzCR0TnhM .label text,#mermaid-svg-IowZ15YRzCR0TnhM span{fill:#333;color:#333;}#mermaid-svg-IowZ15YRzCR0TnhM .node rect,#mermaid-svg-IowZ15YRzCR0TnhM .node circle,#mermaid-svg-IowZ15YRzCR0TnhM .node ellipse,#mermaid-svg-IowZ15YRzCR0TnhM .node polygon,#mermaid-svg-IowZ15YRzCR0TnhM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IowZ15YRzCR0TnhM .rough-node .label text,#mermaid-svg-IowZ15YRzCR0TnhM .node .label text,#mermaid-svg-IowZ15YRzCR0TnhM .image-shape .label,#mermaid-svg-IowZ15YRzCR0TnhM .icon-shape .label{text-anchor:middle;}#mermaid-svg-IowZ15YRzCR0TnhM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IowZ15YRzCR0TnhM .rough-node .label,#mermaid-svg-IowZ15YRzCR0TnhM .node .label,#mermaid-svg-IowZ15YRzCR0TnhM .image-shape .label,#mermaid-svg-IowZ15YRzCR0TnhM .icon-shape .label{text-align:center;}#mermaid-svg-IowZ15YRzCR0TnhM .node.clickable{cursor:pointer;}#mermaid-svg-IowZ15YRzCR0TnhM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IowZ15YRzCR0TnhM .arrowheadPath{fill:#333333;}#mermaid-svg-IowZ15YRzCR0TnhM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IowZ15YRzCR0TnhM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IowZ15YRzCR0TnhM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IowZ15YRzCR0TnhM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IowZ15YRzCR0TnhM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IowZ15YRzCR0TnhM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IowZ15YRzCR0TnhM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IowZ15YRzCR0TnhM .cluster text{fill:#333;}#mermaid-svg-IowZ15YRzCR0TnhM .cluster span{color:#333;}#mermaid-svg-IowZ15YRzCR0TnhM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IowZ15YRzCR0TnhM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IowZ15YRzCR0TnhM rect.text{fill:none;stroke-width:0;}#mermaid-svg-IowZ15YRzCR0TnhM .icon-shape,#mermaid-svg-IowZ15YRzCR0TnhM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IowZ15YRzCR0TnhM .icon-shape p,#mermaid-svg-IowZ15YRzCR0TnhM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IowZ15YRzCR0TnhM .icon-shape .label rect,#mermaid-svg-IowZ15YRzCR0TnhM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IowZ15YRzCR0TnhM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IowZ15YRzCR0TnhM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IowZ15YRzCR0TnhM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第一版 Rust:只有 4 字节,整体错位
280 · bitfields u32(4 字节)
284 · reserved1(2 字节)
286 · industry_type ❌ 错位
C++ 真实布局:6 个字节
280 · trade_status
281 · security_level
282 · trade_type
283 · suspend_flag
284 · ex_dividend_flag
285 · layer_type
286 · reserved1(2 字节)
288 · industry_type(6 字节)
正确译法:
rust
pub trade_status: u8,
pub security_level: u8,
pub trade_type: u8,
pub suspend_flag: u8,
pub ex_dividend_flag: u8,
pub layer_type: u8,
pub reserved1: [u8; 2],
pub industry_type: [u8; 6],
方法论 :凡遇位域,把每个位域展开成它的基类型字段再验算;凡遇 pack(1),每个结构体都手工验算一遍偏移,不能靠"它们通常一致"的运气。
5.5 用测试把布局锁死(重要!)
布局对不对,不要靠肉眼。std::mem::size_of 和 offset_of!(Rust 1.77+)直接写断言:
rust
#[test]
fn test_layout() {
use std::mem::{offset_of, size_of};
assert_eq!(size_of::<XtpOrderInfo>(), 152);
assert_eq!(offset_of!(XtpOrderInfo, price), 48);
assert_eq!(offset_of!(XtpOrderInfo, side), 68);
assert_eq!(offset_of!(XtpOrderInfo, order_status), 140);
assert_eq!(size_of::<XtpNqFullInfo>(), 328);
assert_eq!(offset_of!(XtpNqFullInfo, trade_status), 280);
assert_eq!(offset_of!(XtpNqFullInfo, industry_type), 288);
assert_eq!(size_of::<XtpAsset>(), 416); // 曾经错成 136
assert_eq!(size_of::<XtpStkPosition>(), 536); // 曾经错成 208
assert_eq!(size_of::<XtpMarketData>(), 736);
}
反面教材 :第一版测试只写
assert!(size > 400)这种下限断言,结果 136 字节的XtpAsset也"通过"了。断言必须精确等值,尺寸和关键偏移都要。
第 6 章 枚举与常量映射
XTP 头文件里有三种"枚举",译法略有不同:
#mermaid-svg-mPbgLgA2ATJtPdQs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mPbgLgA2ATJtPdQs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mPbgLgA2ATJtPdQs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mPbgLgA2ATJtPdQs .error-icon{fill:#552222;}#mermaid-svg-mPbgLgA2ATJtPdQs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mPbgLgA2ATJtPdQs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mPbgLgA2ATJtPdQs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mPbgLgA2ATJtPdQs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mPbgLgA2ATJtPdQs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mPbgLgA2ATJtPdQs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mPbgLgA2ATJtPdQs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mPbgLgA2ATJtPdQs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mPbgLgA2ATJtPdQs .marker.cross{stroke:#333333;}#mermaid-svg-mPbgLgA2ATJtPdQs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mPbgLgA2ATJtPdQs p{margin:0;}#mermaid-svg-mPbgLgA2ATJtPdQs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mPbgLgA2ATJtPdQs .cluster-label text{fill:#333;}#mermaid-svg-mPbgLgA2ATJtPdQs .cluster-label span{color:#333;}#mermaid-svg-mPbgLgA2ATJtPdQs .cluster-label span p{background-color:transparent;}#mermaid-svg-mPbgLgA2ATJtPdQs .label text,#mermaid-svg-mPbgLgA2ATJtPdQs span{fill:#333;color:#333;}#mermaid-svg-mPbgLgA2ATJtPdQs .node rect,#mermaid-svg-mPbgLgA2ATJtPdQs .node circle,#mermaid-svg-mPbgLgA2ATJtPdQs .node ellipse,#mermaid-svg-mPbgLgA2ATJtPdQs .node polygon,#mermaid-svg-mPbgLgA2ATJtPdQs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mPbgLgA2ATJtPdQs .rough-node .label text,#mermaid-svg-mPbgLgA2ATJtPdQs .node .label text,#mermaid-svg-mPbgLgA2ATJtPdQs .image-shape .label,#mermaid-svg-mPbgLgA2ATJtPdQs .icon-shape .label{text-anchor:middle;}#mermaid-svg-mPbgLgA2ATJtPdQs .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mPbgLgA2ATJtPdQs .rough-node .label,#mermaid-svg-mPbgLgA2ATJtPdQs .node .label,#mermaid-svg-mPbgLgA2ATJtPdQs .image-shape .label,#mermaid-svg-mPbgLgA2ATJtPdQs .icon-shape .label{text-align:center;}#mermaid-svg-mPbgLgA2ATJtPdQs .node.clickable{cursor:pointer;}#mermaid-svg-mPbgLgA2ATJtPdQs .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mPbgLgA2ATJtPdQs .arrowheadPath{fill:#333333;}#mermaid-svg-mPbgLgA2ATJtPdQs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mPbgLgA2ATJtPdQs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mPbgLgA2ATJtPdQs .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mPbgLgA2ATJtPdQs .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mPbgLgA2ATJtPdQs .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mPbgLgA2ATJtPdQs .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mPbgLgA2ATJtPdQs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mPbgLgA2ATJtPdQs .cluster text{fill:#333;}#mermaid-svg-mPbgLgA2ATJtPdQs .cluster span{color:#333;}#mermaid-svg-mPbgLgA2ATJtPdQs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mPbgLgA2ATJtPdQs .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mPbgLgA2ATJtPdQs rect.text{fill:none;stroke-width:0;}#mermaid-svg-mPbgLgA2ATJtPdQs .icon-shape,#mermaid-svg-mPbgLgA2ATJtPdQs .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mPbgLgA2ATJtPdQs .icon-shape p,#mermaid-svg-mPbgLgA2ATJtPdQs .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mPbgLgA2ATJtPdQs .icon-shape .label rect,#mermaid-svg-mPbgLgA2ATJtPdQs .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mPbgLgA2ATJtPdQs .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mPbgLgA2ATJtPdQs .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mPbgLgA2ATJtPdQs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} C++ 写法
Rust 译法
enum X { A=1, B=2 }
#repr(i32) pub enum X { A=1, B=2 }
typedef uint32_t X;
constexpr uint32_t A=1;
#repr(u32) enum + from_raw() 或直接 pub const
#define XTP_SIDE_BUY 1
pub const SIDE_BUY: u8 = 1;
rust
// 交易枚举(int):
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum MarketType { Init = 0, SZA = 1, SHA = 2, BJA = 3, HK = 4, Unknown = 5 }
// 行情"枚举"(实为 uint32_t typedef):
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum ExchangeType { SH = 1, SZ = 2, NQ = 3, HK = 4, Unknown = 5 }
impl ExchangeType {
pub fn from_raw(v: u32) -> Self { /* match 映射,未知值给 Unknown */ }
}
// #define 常量组(值太多或不连续时比 enum 顺手):
pub type SideType = u8;
pub const SIDE_BUY: u8 = 1;
pub const SIDE_SELL: u8 = 2;
pub const SIDE_MARGIN_TRADE: u8 = 21;
// ...
逐个数字核对! 真实案例:FundTransferType 的 Unknown 在 C++ 里是 8(因为前面有 0~7 共 8 个值),第一版写成了 9------差一个数,跨节点划拨的类型判断就全错。
版本陷阱 :同名枚举在两个 DLL 里可能不同。XTP_EXCHANGE_TYPE 在行情(XTPX)里是 SH=1,SZ=2,NQ=3,HK=4,UNKNOWN=5(uint32_t),在老行情包里却是 SH=1,SZ=2,NQ=3,UNKNOWN=4(enum)。封装哪个 DLL,就只信哪个包的头文件。
第 7 章 回调(SPI)桥:在 Rust 里伪造一个 C++ 对象
到了全教程技术含量最高的部分。前面都是我们调 DLL;现在是 DLL 调我们。
7.1 问题本质
pQuoteApi->RegisterSpi(pQuoteSpi) 期望收到一个 QuoteSpi*------一个前 8 字节是 vtable 指针、vtable 里有 38 个函数指针的 C++ 对象。DLL 收到后,内部线程会这样回调:
spi->OnDepthMarketData(md, bid1_qty, ...);
// 等价于:spi_vtable[4](spi, md, bid1_qty, ...)
所以我们必须手工造出这样的内存布局,并把函数指针指向 Rust 函数:
#mermaid-svg-0UOeAo6YrU6GHqvT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0UOeAo6YrU6GHqvT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0UOeAo6YrU6GHqvT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0UOeAo6YrU6GHqvT .error-icon{fill:#552222;}#mermaid-svg-0UOeAo6YrU6GHqvT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0UOeAo6YrU6GHqvT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0UOeAo6YrU6GHqvT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0UOeAo6YrU6GHqvT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0UOeAo6YrU6GHqvT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0UOeAo6YrU6GHqvT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0UOeAo6YrU6GHqvT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0UOeAo6YrU6GHqvT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0UOeAo6YrU6GHqvT .marker.cross{stroke:#333333;}#mermaid-svg-0UOeAo6YrU6GHqvT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0UOeAo6YrU6GHqvT p{margin:0;}#mermaid-svg-0UOeAo6YrU6GHqvT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0UOeAo6YrU6GHqvT .cluster-label text{fill:#333;}#mermaid-svg-0UOeAo6YrU6GHqvT .cluster-label span{color:#333;}#mermaid-svg-0UOeAo6YrU6GHqvT .cluster-label span p{background-color:transparent;}#mermaid-svg-0UOeAo6YrU6GHqvT .label text,#mermaid-svg-0UOeAo6YrU6GHqvT span{fill:#333;color:#333;}#mermaid-svg-0UOeAo6YrU6GHqvT .node rect,#mermaid-svg-0UOeAo6YrU6GHqvT .node circle,#mermaid-svg-0UOeAo6YrU6GHqvT .node ellipse,#mermaid-svg-0UOeAo6YrU6GHqvT .node polygon,#mermaid-svg-0UOeAo6YrU6GHqvT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0UOeAo6YrU6GHqvT .rough-node .label text,#mermaid-svg-0UOeAo6YrU6GHqvT .node .label text,#mermaid-svg-0UOeAo6YrU6GHqvT .image-shape .label,#mermaid-svg-0UOeAo6YrU6GHqvT .icon-shape .label{text-anchor:middle;}#mermaid-svg-0UOeAo6YrU6GHqvT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0UOeAo6YrU6GHqvT .rough-node .label,#mermaid-svg-0UOeAo6YrU6GHqvT .node .label,#mermaid-svg-0UOeAo6YrU6GHqvT .image-shape .label,#mermaid-svg-0UOeAo6YrU6GHqvT .icon-shape .label{text-align:center;}#mermaid-svg-0UOeAo6YrU6GHqvT .node.clickable{cursor:pointer;}#mermaid-svg-0UOeAo6YrU6GHqvT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0UOeAo6YrU6GHqvT .arrowheadPath{fill:#333333;}#mermaid-svg-0UOeAo6YrU6GHqvT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0UOeAo6YrU6GHqvT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0UOeAo6YrU6GHqvT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0UOeAo6YrU6GHqvT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0UOeAo6YrU6GHqvT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0UOeAo6YrU6GHqvT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0UOeAo6YrU6GHqvT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0UOeAo6YrU6GHqvT .cluster text{fill:#333;}#mermaid-svg-0UOeAo6YrU6GHqvT .cluster span{color:#333;}#mermaid-svg-0UOeAo6YrU6GHqvT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0UOeAo6YrU6GHqvT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0UOeAo6YrU6GHqvT rect.text{fill:none;stroke-width:0;}#mermaid-svg-0UOeAo6YrU6GHqvT .icon-shape,#mermaid-svg-0UOeAo6YrU6GHqvT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0UOeAo6YrU6GHqvT .icon-shape p,#mermaid-svg-0UOeAo6YrU6GHqvT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0UOeAo6YrU6GHqvT .icon-shape .label rect,#mermaid-svg-0UOeAo6YrU6GHqvT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0UOeAo6YrU6GHqvT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0UOeAo6YrU6GHqvT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0UOeAo6YrU6GHqvT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ③ trampoline(Rust 函数)
② 静态 QUOTE_SPI_VTABLE(Rust 数据段)
① QuoteSpiHolder(Rust 分配)
指向
跳转到
回调 spi 的虚函数
vtable 指针
对象的前 8 字节
callback
Box<dyn QuoteSpi>
槽位 0 → tramp_on_disconnected
槽位 1 → tramp_on_error
槽位 2 → tramp_on_sub_market_data
... 共 38 个 trampoline ...
解出 holder.callback
调用对应 trait 方法
XTP DLL 内部线程
7.2 定义 Rust 回调 trait
rust
pub trait QuoteSpi: Send + Sync {
fn on_disconnected(&self, reason: i32) {}
fn on_error(&self, error_info: &XtpRspInfo) {}
fn on_sub_market_data(&self, ticker: &XtpSpecificTicker, error_info: &XtpRspInfo, is_last: bool) {}
fn on_depth_market_data(&self, md: &XtpMarketData,
bid1_qty: &[i64], bid1_count: i32, max_bid1_count: i32,
ask1_qty: &[i64], ask1_count: i32, max_ask1_count: i32) {}
fn on_order_book(&self, order_book: &XtpOrderBook) {}
fn on_tick_by_tick(&self, tbt: &XtpTickByTick) {}
// ... 与 C++ 38 个回调一一对应,全部给默认空实现
}
Send + Sync:回调发生在 DLL 的内部线程,用户的实现必须能跨线程;- 全默认实现:用户只覆盖关心的几个;
#[allow(unused_variables)]:默认空实现的未用参数不报警。
7.3 holder 与 vtable 结构体
rust
// 布局即合同:第一个字段必须是 vtable 指针
#[repr(C)]
struct QuoteSpiHolder {
vtable: *const QuoteSpiVTable,
callback: Box<dyn QuoteSpi>,
}
// vtable:38 个槽位,顺序与 xtpx_quote_api.h 里 QuoteSpi 的声明顺序一致
#[repr(C)]
struct QuoteSpiVTable {
on_disconnected: unsafe extern "C" fn(*mut QuoteSpiHolder, c_int),
on_error: unsafe extern "C" fn(*mut QuoteSpiHolder, *mut XtpRspInfo),
on_sub_market_data: unsafe extern "C" fn(*mut QuoteSpiHolder, *mut XtpSpecificTicker, *mut XtpRspInfo, bool),
on_unsub_market_data: unsafe extern "C" fn(*mut QuoteSpiHolder, *mut XtpSpecificTicker, *mut XtpRspInfo, bool),
on_depth_market_data: unsafe extern "C" fn(*mut QuoteSpiHolder, *mut XtpMarketData, *mut i64, c_int, c_int, *mut i64, c_int, c_int),
// ... 全部 38 个
}
注意每个 trampoline 的第一个参数是 holder 指针 ------因为 C++ 会把 spi(即我们的 &mut QuoteSpiHolder)当 this 传进来。
7.4 写 trampoline:四条军规
rust
// 军规 1:panic 不能跨 FFI。catch_unwind 兜住。
fn guard<F: FnOnce()>(f: F) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
}
// 军规 2:this 可能为 null(防御性)。
// 军规 3:error_info 可能为 null ------ 官方文档明确允许,统一回退到 OK。
// 军规 4:数据指针可能为 null,判空再调。
static OK_RSP: XtpRspInfo = XtpRspInfo { error_id: 0, error_msg: [0; 124] };
unsafe extern "C" fn tramp_on_sub_market_data(
this: *mut QuoteSpiHolder,
ticker: *mut XtpSpecificTicker,
error_info: *mut XtpRspInfo,
is_last: bool,
) {
guard(|| {
let Some(h) = (unsafe { this.as_ref() }) else { return };
let Some(t) = (unsafe { ticker.as_ref() }) else { return };
let rsp = unsafe { error_info.as_ref() }.unwrap_or(&OK_RSP);
h.callback.on_sub_market_data(t, rsp, is_last);
});
}
unsafe extern "C" fn tramp_on_depth_market_data(
this: *mut QuoteSpiHolder, md: *mut XtpMarketData,
bid1_qty: *mut i64, bid1_count: c_int, max_bid1_count: c_int,
ask1_qty: *mut i64, ask1_count: c_int, max_ask1_count: c_int,
) {
guard(|| {
let Some(h) = (unsafe { this.as_ref() }) else { return };
let Some(md) = (unsafe { md.as_ref() }) else { return };
// 原始指针+计数 → 切片;空指针或非正计数给空切片
let bq = slice_from_c(bid1_qty, bid1_count);
let aq = slice_from_c(ask1_qty, ask1_count);
h.callback.on_depth_market_data(md, bq, bid1_count, max_bid1_count, aq, ask1_count, max_ask1_count);
});
}
unsafe fn slice_from_c<'a>(ptr: *const i64, count: c_int) -> &'a [i64] {
if ptr.is_null() || count <= 0 { &[] } else { unsafe { std::slice::from_raw_parts(ptr, count as usize) } }
}
38 个 trampoline 全是这个模式,机械但必须零失误。写完后装进静态表:
rust
static QUOTE_SPI_VTABLE: QuoteSpiVTable = QuoteSpiVTable {
on_disconnected: tramp_on_disconnected,
on_error: tramp_on_error,
on_sub_market_data: tramp_on_sub_market_data,
// ... 38 个,顺序!顺序!顺序!
};
槽位顺序写错的后果 :DLL 调
OnOrderBook(槽位 8)时实际跳到了OnTickByTick(槽位 11)的函数------参数布局不同,轻则数据全错,重则栈错乱崩溃。写完务必和头文件行号逐一对照。
7.5 注册:让 holder 活得比注册久
rust
pub struct QuoteApi {
raw: RawQuoteApi,
spi: Option<Box<QuoteSpiHolder>>, // 持有 holder,地址才稳定
}
pub fn register_spi(&mut self, spi: Box<dyn QuoteSpi>) {
let mut holder = Box::new(QuoteSpiHolder {
vtable: "E_SPI_VTABLE,
callback: spi,
});
// 把 holder 的地址交给 DLL ------ 之后 DLL 会把它当 QuoteSpi* 用
self.raw.register_spi(&mut *holder as *mut QuoteSpiHolder as *mut c_void);
self.spi = Some(holder); // 存进 self,防止被提前释放
}
生命周期铁律:
#mermaid-svg-ygjqKcP91jDkAX0k{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ygjqKcP91jDkAX0k .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ygjqKcP91jDkAX0k .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ygjqKcP91jDkAX0k .error-icon{fill:#552222;}#mermaid-svg-ygjqKcP91jDkAX0k .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ygjqKcP91jDkAX0k .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ygjqKcP91jDkAX0k .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ygjqKcP91jDkAX0k .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ygjqKcP91jDkAX0k .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ygjqKcP91jDkAX0k .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ygjqKcP91jDkAX0k .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ygjqKcP91jDkAX0k .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ygjqKcP91jDkAX0k .marker.cross{stroke:#333333;}#mermaid-svg-ygjqKcP91jDkAX0k svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ygjqKcP91jDkAX0k p{margin:0;}#mermaid-svg-ygjqKcP91jDkAX0k .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ygjqKcP91jDkAX0k .cluster-label text{fill:#333;}#mermaid-svg-ygjqKcP91jDkAX0k .cluster-label span{color:#333;}#mermaid-svg-ygjqKcP91jDkAX0k .cluster-label span p{background-color:transparent;}#mermaid-svg-ygjqKcP91jDkAX0k .label text,#mermaid-svg-ygjqKcP91jDkAX0k span{fill:#333;color:#333;}#mermaid-svg-ygjqKcP91jDkAX0k .node rect,#mermaid-svg-ygjqKcP91jDkAX0k .node circle,#mermaid-svg-ygjqKcP91jDkAX0k .node ellipse,#mermaid-svg-ygjqKcP91jDkAX0k .node polygon,#mermaid-svg-ygjqKcP91jDkAX0k .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ygjqKcP91jDkAX0k .rough-node .label text,#mermaid-svg-ygjqKcP91jDkAX0k .node .label text,#mermaid-svg-ygjqKcP91jDkAX0k .image-shape .label,#mermaid-svg-ygjqKcP91jDkAX0k .icon-shape .label{text-anchor:middle;}#mermaid-svg-ygjqKcP91jDkAX0k .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ygjqKcP91jDkAX0k .rough-node .label,#mermaid-svg-ygjqKcP91jDkAX0k .node .label,#mermaid-svg-ygjqKcP91jDkAX0k .image-shape .label,#mermaid-svg-ygjqKcP91jDkAX0k .icon-shape .label{text-align:center;}#mermaid-svg-ygjqKcP91jDkAX0k .node.clickable{cursor:pointer;}#mermaid-svg-ygjqKcP91jDkAX0k .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ygjqKcP91jDkAX0k .arrowheadPath{fill:#333333;}#mermaid-svg-ygjqKcP91jDkAX0k .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ygjqKcP91jDkAX0k .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ygjqKcP91jDkAX0k .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ygjqKcP91jDkAX0k .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ygjqKcP91jDkAX0k .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ygjqKcP91jDkAX0k .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ygjqKcP91jDkAX0k .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ygjqKcP91jDkAX0k .cluster text{fill:#333;}#mermaid-svg-ygjqKcP91jDkAX0k .cluster span{color:#333;}#mermaid-svg-ygjqKcP91jDkAX0k div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ygjqKcP91jDkAX0k .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ygjqKcP91jDkAX0k rect.text{fill:none;stroke-width:0;}#mermaid-svg-ygjqKcP91jDkAX0k .icon-shape,#mermaid-svg-ygjqKcP91jDkAX0k .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ygjqKcP91jDkAX0k .icon-shape p,#mermaid-svg-ygjqKcP91jDkAX0k .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ygjqKcP91jDkAX0k .icon-shape .label rect,#mermaid-svg-ygjqKcP91jDkAX0k .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ygjqKcP91jDkAX0k .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ygjqKcP91jDkAX0k .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ygjqKcP91jDkAX0k :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 绝不早于
register_spi
DLL 持有 holder 地址
holder 何时能释放?
raw.Release()
(DLL 停止回调线程)
再释放 holder
Rust 结构体字段按声明顺序 drop ,自定义 Drop::drop 又先于字段 drop 执行,所以:
rust
pub struct QuoteApi {
raw: RawQuoteApi, // 声明在前:先 Release,回调停止
spi: Option<Box<QuoteSpiHolder>>,// 声明在后:后释放 holder
}
// RawQuoteApi 的 Drop 里调 (vtable.release)(obj)
这个顺序是安全性的一部分,要写注释锁住,防止后来者重排字段。
7.6 交易 SPI:65 个槽位怎么消化
TraderSpi 有 65 个虚函数,我们只想暴露 13 个常用回调。其余 52 个槽位也必须有 ABI 正确的占位函数,因为 DLL 可能调其中任何一个:
rust
macro_rules! stub {
($name:ident ( $($t:ty),* )) => {
unsafe extern "C" fn $name(_this: *mut TraderSpiHolder $(, _: $t)*) {}
};
}
stub!(noop_on_query_order_ex(*mut c_void, *mut XtpRspInfo, c_int, bool, u64));
stub!(noop_on_query_order_by_page(*mut c_void, i64, i64, i64, c_int, bool, u64));
// ... 52 个,参数个数/类型与头文件一致(未封装的结构体统一 *mut c_void)
为什么 stub 也要参数正确?x64 上前 4 个参数在寄存器,其余在栈上。函数签名决定被调方如何理解栈布局。stub 什么都不做虽然不读参数,但保持签名一致是零成本的保险,万一以后要在 stub 里加日志也不会踩雷。
7.7 回调里的工程纪律(写给使用者)
封装文档里必须警告用户:
- 回调在 DLL 线程里跑,必须快速返回 。要处理数据就
channel.send()丢给自己的线程。官方 FAQ 明确:回调处理过慢会填满接收缓冲区,服务器会断开连接(UDP 下则直接丢包); - 回调里不要 再调同一 API 的阻塞函数(可能死锁),官方文档注明
Logout不允许在回调线程调; - 回调参数(
&XtpMarketData等)只在调用期间有效------官方 FAQ 232 条明确警告"只保存指针不拷贝数据,之后读到的是被复用的内存"。Rust 的&T+clone在类型层面就强制了这一点(C++ 里靠自觉); - 断线回调
on_disconnected里可以重新Login(官方推荐做法,见第 12 章),但不要 在回调里调Release或Logout; - 屏幕输出(
println!)是耗时操作,行情高峰(每秒约 20 万笔)时千万别在回调里逐条打印------官方 FAQ 58 条把"有屏幕输出"列为全订阅断线的首要嫌疑; - 回调线程不止一个 (官方 FAQ 146-149):QuoteSpi 在 TCP 下是 1 个线程,UDP 下是 1 个 TCP 线程 + 每组播组 1 个 UDP 线程;TraderSpi 的查询回调是 1 个线程,但
OnOrderEvent/OnTradeEvent可能由正常线程 + 超时线程 两个线程回调。所以 trait 要求Send + Sync,用户实现里的共享状态必须自己加锁(Mutex)或用无锁结构; - XTP 字符串(
ticker_name等)是 UTF-8 编码 。RustString天然 UTF-8,to_string_lossy直接正确------这点反而比 C++ 省心(C++ demo 在 GBK 控制台上打印中文是乱码)。
第 8 章 安全封装层设计
裸 FFI 能跑了,现在把它包成"用着不会错"的 Rust API。
8.1 RAII 与 Drop
rust
impl Drop for RawQuoteApi {
fn drop(&mut self) {
unsafe { (self.vt().release)(self.obj); }
}
}
要点:
_library: Arc<Library>保证 DLL 在对象存续期间不卸载;Drop::drop先于字段 drop 执行,所以release时 DLL 必然还在;vtable用裸指针 而不是&'static------vtable 内存属于 DLL,标'static是生命周期撒谎(第一版的真实问题);Release只允许调一次 。不要提供公开的release()然后又让 Drop 再调一次。要么ManuallyDrop,要么把 release 完全交给 Drop(本文选择后者)。
8.2 线程安全:Send/Sync
rust
// XTP 官方文档声明 API 线程安全。对象归 DLL 管,DLL 由 _library 保活。
unsafe impl Send for RawQuoteApi {}
unsafe impl Sync for RawQuoteApi {}
这样 QuoteApi/TraderApi 才能放进 Arc、跨线程调用------量化程序里策略线程、风控线程都要碰它。
8.3 错误类型
rust
#[derive(Debug, thiserror::Error)]
pub enum XtpError {
#[error("Failed to load DLL '{dll}': {source}")]
DllLoadError { dll: String, source: libloading::Error },
#[error("Symbol '{symbol}' not found in DLL '{dll}'")]
SymbolNotFound { dll: String, symbol: String },
#[error("API call failed: {0}")]
ApiError(String),
#[error("XTP error: {0}")]
XtpRspError(XtpRspInfo), // 柜台返回的业务错误(error_id + 中文消息)
#[error("Not logged in")]
NotLoggedIn,
#[error("Invalid argument: {0}")]
InvalidArgument(String),
#[error("Nul byte error: {0}")]
NulError(#[from] std::ffi::NulError),
}
所有 CString::new 都走 map_err 而不是 unwrap()------第一版在 login 里 unwrap(),一个含 NUL 的异常输入就跨 FFI panic。
8.4 会回写的参数必须用 &mut(血的教训)
C++ 的下单接口:
cpp
virtual uint64_t InsertOrder(XTPOrderInsertInfo *order, uint64_t session_id) = 0;
// ^^^^^ API 会把分配到的 order_xtp_id 写回这个结构体
第一版封装:
rust
pub fn insert_order(&self, order: &XtpOrderInsertInfo) -> Result<u64, XtpError> {
self.raw.insert_order(order as *const _ as *mut _, sid) // ❌ &T 强转 *mut T 让 C++ 写入
}
&T 在 Rust 里承诺"内存不会被写",把它交给会写的 C++ 是未定义行为(编译器可能基于只读假设做优化)。正确做法:
rust
pub fn insert_order(&self, order: &mut XtpOrderInsertInfo) -> Result<u64, XtpError> {
let sid = self.session_id.ok_or(XtpError::NotLoggedIn)?;
let xtp_id = self.raw.insert_order(order as *mut _, sid);
if xtp_id == 0 { Err(XtpError::XtpRspError(self.raw.get_api_last_error())) } else { Ok(xtp_id) }
}
// fund_transfer(&mut XtpFundTransferReq) 同理:C++ 回写 serial_id。
判断规则 :看 C++ 参数是不是非 const 指针。非 const ⇒ 可能写 ⇒ Rust 侧用 &mut。
8.5 会话与请求编号
rust
pub struct TraderApi {
raw: RawTraderApi,
spi: Option<Box<TraderSpiHolder>>,
session_id: Option<u64>, // login 成功后才有
next_request_id: Mutex<i32>, // 查询类接口的 request_id 自增器
}
pub fn login(&mut self, ...) -> Result<u64, XtpError> {
let sid = self.raw.login(...)?;
if sid > 0 { self.session_id = Some(sid); }
Ok(sid)
}
pub fn query_asset(&self) -> Result<i32, XtpError> {
let sid = self.session_id.ok_or(XtpError::NotLoggedIn)?;
let req_id = self.next_req_id();
self.raw.query_asset(sid, req_id)?;
Ok(req_id) // 返回给调用者,用于和回调里的 request_id 对账
}
官方规则补充 :
session_id在进程重启后相同,但断线重连后会变化 ------重连成功后必须更新保存的session_id(见第 12.4 节)。另外官方 FAQ 185 条指出GetApiLastError不是线程安全的(其余接口都是),多线程并发取错时注意。
8.6 字符串入参:ticker 数组
订阅接口要 char* ticker[](指针数组)。正确姿势:
rust
fn sub_cstrs(tickers: &[&str]) -> Result<(Vec<CString>, Vec<*mut c_char>), XtpError> {
let cs: Vec<CString> = tickers.iter()
.map(|t| CString::new(*t).map_err(|e| XtpError::InvalidArgument(e.to_string())))
.collect::<Result<_, _>>()?;
let ptrs: Vec<*mut c_char> = cs.iter().map(|t| t.as_ptr() as *mut c_char).collect();
Ok((cs, ptrs)) // 两个 Vec 一起返回,cs 保活字符串内存
}
// 调用时:
let (_cs, mut ptrs) = Self::sub_cstrs(tickers)?;
let r = unsafe { vfn(self.obj, ptrs.as_mut_ptr(), ptrs.len() as c_int, ex as u32) };
// _cs 活过这次 FFI 调用,指针才有效
铁律 :C 字符串内存必须活过 FFI 调用的整个时长。cs 若在调用前 drop,指针全部悬垂。
第 9 章 踩坑实录:21 个真实 bug 的分类清单
以下每一条都来自对第一版封装的真实质检。这比正面说教更有价值------它们是 FFI 项目的"事故树"。
#mermaid-svg-rHe7KmE1rq2BDj6R{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rHe7KmE1rq2BDj6R .error-icon{fill:#552222;}#mermaid-svg-rHe7KmE1rq2BDj6R .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rHe7KmE1rq2BDj6R .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rHe7KmE1rq2BDj6R .marker.cross{stroke:#333333;}#mermaid-svg-rHe7KmE1rq2BDj6R svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rHe7KmE1rq2BDj6R p{margin:0;}#mermaid-svg-rHe7KmE1rq2BDj6R .edge{stroke-width:3;}#mermaid-svg-rHe7KmE1rq2BDj6R .section--1 rect,#mermaid-svg-rHe7KmE1rq2BDj6R .section--1 path,#mermaid-svg-rHe7KmE1rq2BDj6R .section--1 circle,#mermaid-svg-rHe7KmE1rq2BDj6R .section--1 polygon,#mermaid-svg-rHe7KmE1rq2BDj6R .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .section--1 text{fill:#ffffff;}#mermaid-svg-rHe7KmE1rq2BDj6R .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-depth--1{stroke-width:17;}#mermaid-svg-rHe7KmE1rq2BDj6R .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled circle,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:lightgray;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:#efefef;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-0 rect,#mermaid-svg-rHe7KmE1rq2BDj6R .section-0 path,#mermaid-svg-rHe7KmE1rq2BDj6R .section-0 circle,#mermaid-svg-rHe7KmE1rq2BDj6R .section-0 polygon,#mermaid-svg-rHe7KmE1rq2BDj6R .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-rHe7KmE1rq2BDj6R .section-0 text{fill:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .node-icon-0{font-size:40px;color:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-depth-0{stroke-width:14;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled circle,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:lightgray;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:#efefef;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-1 rect,#mermaid-svg-rHe7KmE1rq2BDj6R .section-1 path,#mermaid-svg-rHe7KmE1rq2BDj6R .section-1 circle,#mermaid-svg-rHe7KmE1rq2BDj6R .section-1 polygon,#mermaid-svg-rHe7KmE1rq2BDj6R .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .section-1 text{fill:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .node-icon-1{font-size:40px;color:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-depth-1{stroke-width:11;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled circle,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:lightgray;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:#efefef;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-2 rect,#mermaid-svg-rHe7KmE1rq2BDj6R .section-2 path,#mermaid-svg-rHe7KmE1rq2BDj6R .section-2 circle,#mermaid-svg-rHe7KmE1rq2BDj6R .section-2 polygon,#mermaid-svg-rHe7KmE1rq2BDj6R .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .section-2 text{fill:#ffffff;}#mermaid-svg-rHe7KmE1rq2BDj6R .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-depth-2{stroke-width:8;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled circle,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:lightgray;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:#efefef;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-3 rect,#mermaid-svg-rHe7KmE1rq2BDj6R .section-3 path,#mermaid-svg-rHe7KmE1rq2BDj6R .section-3 circle,#mermaid-svg-rHe7KmE1rq2BDj6R .section-3 polygon,#mermaid-svg-rHe7KmE1rq2BDj6R .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .section-3 text{fill:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .node-icon-3{font-size:40px;color:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-depth-3{stroke-width:5;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled circle,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:lightgray;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:#efefef;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-4 rect,#mermaid-svg-rHe7KmE1rq2BDj6R .section-4 path,#mermaid-svg-rHe7KmE1rq2BDj6R .section-4 circle,#mermaid-svg-rHe7KmE1rq2BDj6R .section-4 polygon,#mermaid-svg-rHe7KmE1rq2BDj6R .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .section-4 text{fill:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .node-icon-4{font-size:40px;color:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-depth-4{stroke-width:2;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled circle,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:lightgray;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:#efefef;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-5 rect,#mermaid-svg-rHe7KmE1rq2BDj6R .section-5 path,#mermaid-svg-rHe7KmE1rq2BDj6R .section-5 circle,#mermaid-svg-rHe7KmE1rq2BDj6R .section-5 polygon,#mermaid-svg-rHe7KmE1rq2BDj6R .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .section-5 text{fill:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .node-icon-5{font-size:40px;color:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-depth-5{stroke-width:-1;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled circle,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:lightgray;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:#efefef;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-6 rect,#mermaid-svg-rHe7KmE1rq2BDj6R .section-6 path,#mermaid-svg-rHe7KmE1rq2BDj6R .section-6 circle,#mermaid-svg-rHe7KmE1rq2BDj6R .section-6 polygon,#mermaid-svg-rHe7KmE1rq2BDj6R .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .section-6 text{fill:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .node-icon-6{font-size:40px;color:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-depth-6{stroke-width:-4;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled circle,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:lightgray;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:#efefef;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-7 rect,#mermaid-svg-rHe7KmE1rq2BDj6R .section-7 path,#mermaid-svg-rHe7KmE1rq2BDj6R .section-7 circle,#mermaid-svg-rHe7KmE1rq2BDj6R .section-7 polygon,#mermaid-svg-rHe7KmE1rq2BDj6R .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .section-7 text{fill:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .node-icon-7{font-size:40px;color:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-depth-7{stroke-width:-7;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled circle,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:lightgray;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:#efefef;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-8 rect,#mermaid-svg-rHe7KmE1rq2BDj6R .section-8 path,#mermaid-svg-rHe7KmE1rq2BDj6R .section-8 circle,#mermaid-svg-rHe7KmE1rq2BDj6R .section-8 polygon,#mermaid-svg-rHe7KmE1rq2BDj6R .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .section-8 text{fill:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .node-icon-8{font-size:40px;color:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-depth-8{stroke-width:-10;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled circle,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:lightgray;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:#efefef;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-9 rect,#mermaid-svg-rHe7KmE1rq2BDj6R .section-9 path,#mermaid-svg-rHe7KmE1rq2BDj6R .section-9 circle,#mermaid-svg-rHe7KmE1rq2BDj6R .section-9 polygon,#mermaid-svg-rHe7KmE1rq2BDj6R .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .section-9 text{fill:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .node-icon-9{font-size:40px;color:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-depth-9{stroke-width:-13;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled circle,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:lightgray;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:#efefef;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-10 rect,#mermaid-svg-rHe7KmE1rq2BDj6R .section-10 path,#mermaid-svg-rHe7KmE1rq2BDj6R .section-10 circle,#mermaid-svg-rHe7KmE1rq2BDj6R .section-10 polygon,#mermaid-svg-rHe7KmE1rq2BDj6R .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .section-10 text{fill:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .node-icon-10{font-size:40px;color:black;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .edge-depth-10{stroke-width:-16;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled circle,#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:lightgray;}#mermaid-svg-rHe7KmE1rq2BDj6R .disabled text{fill:#efefef;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-root rect,#mermaid-svg-rHe7KmE1rq2BDj6R .section-root path,#mermaid-svg-rHe7KmE1rq2BDj6R .section-root circle,#mermaid-svg-rHe7KmE1rq2BDj6R .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-rHe7KmE1rq2BDj6R .section-root text{fill:#ffffff;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-root span{color:#ffffff;}#mermaid-svg-rHe7KmE1rq2BDj6R .section-2 span{color:#ffffff;}#mermaid-svg-rHe7KmE1rq2BDj6R .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-rHe7KmE1rq2BDj6R .edge{fill:none;}#mermaid-svg-rHe7KmE1rq2BDj6R .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-rHe7KmE1rq2BDj6R :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 21 个真实 bug
功能缺失
SPI 从未注册------最致命
set_udp_thread_affinity 空实现
query_account_trade_market 未包安全层
布局错误
XtpAsset 136B vs 416B
XtpStkPosition 208B vs 536B
XtpNqFullInfo 位域 4B vs 6B
XtpOrderInfo 多 7B
XtpFundTransferNotice 多 8B
XtpQueryOrderRsp 臆造 padding
臆造 XtpTickerInfo
数值错误
FundTransferType.Unknown 应为 8 错写成 9
PositionEffect 缺 4 个信用值
未定义行为
共享引用强转可写指针供 C++ 回写
vtable 生命周期撒谎
CString unwrap 跨 FFI panic
工程问题
硬编码个人路径
真实账号密码进 git
测试断言太松只比下限
126 个编译警告
9.1 重点讲解三条
第 1 名:SPI 从未注册。 register_spi 把回调存进 Arc<Mutex<Box>> 就结束了。编译通过、测试通过、demo 能登录------但任何回调都不来。这类"看起来完整实则断芯"的 bug 只有端到端冒烟测试能抓出来。
第 2 名:结构体"差不多就行"。 XtpAsset 只写了前 13 个字段,刚好覆盖 demo 用到的 total_asset/buying_power,于是"能用"。直到有人读 repay_stock_aval_banlance(信用账户余额)才发现拿到的是别的字段。**FFI 结构体必须 100% 翻译,包括保留字段。**保留位不是装饰,是布局的一部分。
第 3 名:测试自欺欺人。 assert!(size_of::<T>() > 400)------136 字节的错误结构体也满足。改成 == 416 后立刻现形。布局断言必须等值,并抽查关键偏移。
9.2 DLL 的怪癖(不是我们的 bug,但必须知道)
实测 XTPX 行情 DLL(1.2.1-r.3):
从未调用过
Login()的 QuoteApi 对象,直接Release()会在 DLL 内部崩溃(0xC0000005)。 登录尝试过一次(哪怕失败)后,Release正常。
原因在 DLL 内部(某些状态只在 Login 路径上初始化),官方 demo 总是先登录所以不触发。对策:
- 封装文档里写明:创建后请先 Login 再 Release;
- 测试用例按"创建 → 尝试登录 → 释放"的顺序写;
- 交易 DLL 无此问题。
另外观察到:同一进程内极快速反复创建/销毁多个 API 实例,DLL 偶发崩溃(线程竞态)。实务上一个进程内的 API 实例应长寿命,不要频繁重建。
9.3 凭据安全
第一版 examples/quote_demo.rs 里硬编码了真实测试账号和密码并提交了 git。规矩:
rust
fn env_or(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.to_string())
}
let user = env_or("XTP_TRADER_USER", "your_username");
let password = env_or("XTP_TRADER_PASSWORD", "your_password");
第 10 章 测试与调试方法论
10.1 三层测试体系
#mermaid-svg-AE2YjiJgX7bQaGbh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AE2YjiJgX7bQaGbh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AE2YjiJgX7bQaGbh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AE2YjiJgX7bQaGbh .error-icon{fill:#552222;}#mermaid-svg-AE2YjiJgX7bQaGbh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AE2YjiJgX7bQaGbh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AE2YjiJgX7bQaGbh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AE2YjiJgX7bQaGbh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AE2YjiJgX7bQaGbh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AE2YjiJgX7bQaGbh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AE2YjiJgX7bQaGbh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AE2YjiJgX7bQaGbh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AE2YjiJgX7bQaGbh .marker.cross{stroke:#333333;}#mermaid-svg-AE2YjiJgX7bQaGbh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AE2YjiJgX7bQaGbh p{margin:0;}#mermaid-svg-AE2YjiJgX7bQaGbh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AE2YjiJgX7bQaGbh .cluster-label text{fill:#333;}#mermaid-svg-AE2YjiJgX7bQaGbh .cluster-label span{color:#333;}#mermaid-svg-AE2YjiJgX7bQaGbh .cluster-label span p{background-color:transparent;}#mermaid-svg-AE2YjiJgX7bQaGbh .label text,#mermaid-svg-AE2YjiJgX7bQaGbh span{fill:#333;color:#333;}#mermaid-svg-AE2YjiJgX7bQaGbh .node rect,#mermaid-svg-AE2YjiJgX7bQaGbh .node circle,#mermaid-svg-AE2YjiJgX7bQaGbh .node ellipse,#mermaid-svg-AE2YjiJgX7bQaGbh .node polygon,#mermaid-svg-AE2YjiJgX7bQaGbh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AE2YjiJgX7bQaGbh .rough-node .label text,#mermaid-svg-AE2YjiJgX7bQaGbh .node .label text,#mermaid-svg-AE2YjiJgX7bQaGbh .image-shape .label,#mermaid-svg-AE2YjiJgX7bQaGbh .icon-shape .label{text-anchor:middle;}#mermaid-svg-AE2YjiJgX7bQaGbh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AE2YjiJgX7bQaGbh .rough-node .label,#mermaid-svg-AE2YjiJgX7bQaGbh .node .label,#mermaid-svg-AE2YjiJgX7bQaGbh .image-shape .label,#mermaid-svg-AE2YjiJgX7bQaGbh .icon-shape .label{text-align:center;}#mermaid-svg-AE2YjiJgX7bQaGbh .node.clickable{cursor:pointer;}#mermaid-svg-AE2YjiJgX7bQaGbh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AE2YjiJgX7bQaGbh .arrowheadPath{fill:#333333;}#mermaid-svg-AE2YjiJgX7bQaGbh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AE2YjiJgX7bQaGbh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AE2YjiJgX7bQaGbh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AE2YjiJgX7bQaGbh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AE2YjiJgX7bQaGbh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AE2YjiJgX7bQaGbh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AE2YjiJgX7bQaGbh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AE2YjiJgX7bQaGbh .cluster text{fill:#333;}#mermaid-svg-AE2YjiJgX7bQaGbh .cluster span{color:#333;}#mermaid-svg-AE2YjiJgX7bQaGbh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AE2YjiJgX7bQaGbh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AE2YjiJgX7bQaGbh rect.text{fill:none;stroke-width:0;}#mermaid-svg-AE2YjiJgX7bQaGbh .icon-shape,#mermaid-svg-AE2YjiJgX7bQaGbh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AE2YjiJgX7bQaGbh .icon-shape p,#mermaid-svg-AE2YjiJgX7bQaGbh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AE2YjiJgX7bQaGbh .icon-shape .label rect,#mermaid-svg-AE2YjiJgX7bQaGbh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AE2YjiJgX7bQaGbh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AE2YjiJgX7bQaGbh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AE2YjiJgX7bQaGbh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第 3 层:联机测试(需要测试账号)
登录 → 订阅 → 收行情
下单 → 撤单 → 查询
第 2 层:DLL 冒烟测试(需要 DLL,无需账号)
加载 + 创建 + GetApiVersion + 释放
注册 SPI 不崩溃
登录失败能拿到错误消息
第 1 层:静态布局测试(无需 DLL)
size_of / offset_of 等值断言
枚举值断言
字符串辅助函数单测
rust
#[test]
#[ignore = "Requires XTP DLLs"]
fn test_quote_api_create_and_destroy() {
let api = QuoteApi::new(1, "./test_data", LogLevel::Debug, true).unwrap();
assert!(!api.get_api_version().is_empty());
// 怪癖规避:先尝试登录再释放(见 9.2)
let _ = api.login("127.0.0.1", 6001, "none", "none", ProtocolType::Tcp, None);
}
#[ignore] 标注需要 DLL 的测试,平时 cargo test 只跑第 1 层,cargo test -- --include-ignored 全量。
10.2 FFI 崩溃调试法(真实案例复盘)
遇到 0xC0000005(访问冲突)不要慌,按这个流程:
#mermaid-svg-yUmeaui6EHVk564P{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yUmeaui6EHVk564P .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yUmeaui6EHVk564P .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yUmeaui6EHVk564P .error-icon{fill:#552222;}#mermaid-svg-yUmeaui6EHVk564P .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yUmeaui6EHVk564P .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yUmeaui6EHVk564P .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yUmeaui6EHVk564P .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yUmeaui6EHVk564P .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yUmeaui6EHVk564P .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yUmeaui6EHVk564P .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yUmeaui6EHVk564P .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yUmeaui6EHVk564P .marker.cross{stroke:#333333;}#mermaid-svg-yUmeaui6EHVk564P svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yUmeaui6EHVk564P p{margin:0;}#mermaid-svg-yUmeaui6EHVk564P .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yUmeaui6EHVk564P .cluster-label text{fill:#333;}#mermaid-svg-yUmeaui6EHVk564P .cluster-label span{color:#333;}#mermaid-svg-yUmeaui6EHVk564P .cluster-label span p{background-color:transparent;}#mermaid-svg-yUmeaui6EHVk564P .label text,#mermaid-svg-yUmeaui6EHVk564P span{fill:#333;color:#333;}#mermaid-svg-yUmeaui6EHVk564P .node rect,#mermaid-svg-yUmeaui6EHVk564P .node circle,#mermaid-svg-yUmeaui6EHVk564P .node ellipse,#mermaid-svg-yUmeaui6EHVk564P .node polygon,#mermaid-svg-yUmeaui6EHVk564P .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yUmeaui6EHVk564P .rough-node .label text,#mermaid-svg-yUmeaui6EHVk564P .node .label text,#mermaid-svg-yUmeaui6EHVk564P .image-shape .label,#mermaid-svg-yUmeaui6EHVk564P .icon-shape .label{text-anchor:middle;}#mermaid-svg-yUmeaui6EHVk564P .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yUmeaui6EHVk564P .rough-node .label,#mermaid-svg-yUmeaui6EHVk564P .node .label,#mermaid-svg-yUmeaui6EHVk564P .image-shape .label,#mermaid-svg-yUmeaui6EHVk564P .icon-shape .label{text-align:center;}#mermaid-svg-yUmeaui6EHVk564P .node.clickable{cursor:pointer;}#mermaid-svg-yUmeaui6EHVk564P .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yUmeaui6EHVk564P .arrowheadPath{fill:#333333;}#mermaid-svg-yUmeaui6EHVk564P .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yUmeaui6EHVk564P .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yUmeaui6EHVk564P .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yUmeaui6EHVk564P .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yUmeaui6EHVk564P .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yUmeaui6EHVk564P .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yUmeaui6EHVk564P .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yUmeaui6EHVk564P .cluster text{fill:#333;}#mermaid-svg-yUmeaui6EHVk564P .cluster span{color:#333;}#mermaid-svg-yUmeaui6EHVk564P div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yUmeaui6EHVk564P .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yUmeaui6EHVk564P rect.text{fill:none;stroke-width:0;}#mermaid-svg-yUmeaui6EHVk564P .icon-shape,#mermaid-svg-yUmeaui6EHVk564P .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yUmeaui6EHVk564P .icon-shape p,#mermaid-svg-yUmeaui6EHVk564P .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yUmeaui6EHVk564P .icon-shape .label rect,#mermaid-svg-yUmeaui6EHVk564P .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yUmeaui6EHVk564P .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yUmeaui6EHVk564P .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yUmeaui6EHVk564P :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 我们的错
DLL 的错
崩溃
- 最小复现
写一个 20 行的 main 单独跑
2. 加无缓冲日志 eprintln!
定位崩在哪一行
3. 二分变量
mem::forget 跳过 Release?
不加载 SPI?
4. 对照实验
官方 C++ demo 同操作崩不崩?
结论
修布局/签名/生命周期
记录怪癖 + 规避路径
复盘我们遇到的崩溃:
- 现象:创建 QuoteApi → 打印版本(成功)→ drop 时崩;
mem::forget(不 Release):不崩 ⇒ 问题在 Release 或 DLL 卸载;- 显式
release()后立刻崩 ⇒ Release 内部崩; - 官方 demo 同路径(先登录)不崩 ⇒ DLL 对"未登录"路径支持不良,记入怪癖清单。
这套"最小复现 + 变量隔离 + 对照组"的流程,比上调试器盲猜快得多。
10.3 验证清单(发布前逐项打勾)
-
cargo check --all-targets:0 错误 0 警告 - 每个结构体都有
size_of等值断言 - 关键结构体抽查
offset_of!断言 - 枚举值与头文件逐一核对
- vtable 槽位与头文件行号逐一核对(Api 和 Spi 两张表)
- dumpbin 验证工厂函数修饰名
- DLL 冒烟测试通过(版本号、错误传播、SPI 注册)
- 回调端到端收到过真实数据(联机)
- 无凭据、无个人路径入库
第 11 章 完整使用示例
最终,使用者看到的是这样的 Rust 代码(与第 1 章 C++ demo 逐步对应):
rust
use xtp_api::common::*;
use xtp_api::quote::*;
struct MySpi;
impl QuoteSpi for MySpi {
fn on_depth_market_data(&self, md: &XtpMarketData,
bid1: &[i64], _: i32, _: i32, ask1: &[i64], _: i32, _: i32) {
println!("{} 最新价 {:.2} 买一 {} x {:?}", md.ticker_str(), md.last_price, md.bid[0], bid1);
}
fn on_disconnected(&self, reason: i32) {
eprintln!("断线! reason={}", reason);
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 凭据从环境变量读(不要硬编码进代码库!)
let user = std::env::var("XTP_QUOTE_USER")?;
let pass = std::env::var("XTP_QUOTE_PASSWORD")?;
// 1. 创建(对应 CreateQuoteApi)
let mut api = QuoteApi::new(1, "./data", LogLevel::Debug, true)?;
println!("API 版本: {}", api.get_api_version());
// 2. 配置(必须在 Login 前)
api.set_config_file("./quote_config.ini")?; // 注意:官方要求绝对路径
api.set_heart_beat_interval(30);
// 3. 注册回调(对应 RegisterSpi,必须在 Login 前)
api.register_spi(Box::new(MySpi));
// 4. 登录(同步阻塞)
api.login("119.3.103.38", 6002, &user, &pass, ProtocolType::Tcp, None)?;
// 5. 订阅
api.subscribe_market_data(&["600000"], ExchangeType::SH)?;
api.subscribe_tick_by_tick(&["000001"], ExchangeType::SZ)?;
// 6. 跑事件循环(回调在 DLL 线程里到达)
std::thread::sleep(std::time::Duration::from_secs(60));
// 7. 登出(对应 Logout;Release 由 Drop 自动完成)
api.logout()?;
Ok(())
}
交易侧:
rust
use xtp_api::common::*;
use xtp_api::trader::*;
struct MyTradeSpi;
impl TraderSpi for MyTradeSpi {
fn on_order_event(&self, order: &XtpOrderInfo, err: &XtpRspInfo, _sid: u64) {
println!("报单 {} 状态={}", order.order_xtp_id, order.order_status);
}
fn on_trade_event(&self, trade: &XtpTradeReport, _sid: u64) {
println!("成交 {} 价格={:.2} 数量={}", trade.order_xtp_id, trade.price, trade.quantity);
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 凭据从环境变量读(不要硬编码进代码库!)
let (ip, user, pass, key) = (
std::env::var("XTP_TRADER_IP")?,
std::env::var("XTP_TRADER_USER")?,
std::env::var("XTP_TRADER_PASSWORD")?,
std::env::var("XTP_SOFTWARE_KEY")?,
);
let mut api = TraderApi::new(1, "./data", LogLevel::Debug)?;
api.set_software_key(&key)?;
api.set_software_version("rust-xtp-0.2")?;
api.subscribe_public_topic(ResumeType::Quick);
api.register_spi(Box::new(MyTradeSpi));
let session = api.login(&ip, 6102, &user, &pass, ProtocolType::Tcp, None)?;
if session == 0 { panic!("登录失败: {}", api.get_api_last_error()); }
// 下单(注意 &mut:C++ 会回写 order_xtp_id)
let mut order = XtpOrderInsertInfo::default();
order.set_ticker("600000");
order.market = MarketType::SHA as i32;
order.price = 10.50;
order.quantity = 100;
order.price_type = PriceType::Limit as i32;
order.side = SIDE_BUY;
order.business_type = BusinessType::Cash as i32;
let xtp_id = api.insert_order(&mut order)?;
// 撤单
api.cancel_order(xtp_id)?;
// 查询(回调 on_query_asset / on_query_position 里收结果)
api.query_asset()?;
api.query_position(None, MarketType::Init)?;
api.logout()?;
Ok(())
}
附录 A 速查表
A.1 封装一个 C++ 接口的固定动作
| 步骤 | 动作 | 工具 |
|---|---|---|
| 1 | 找到头文件,确定 pack 值 | #pragma pack |
| 2 | 抄虚函数顺序成 vtable 结构体 | 头文件行号 |
| 3 | dumpbin 抄工厂函数修饰名 | dumpbin /exports |
| 4 | 逐字段翻译结构体+写 size/offset 断言 | 手工验算表 |
| 5 | 逐值翻译枚举 | 头文件 |
| 6 | 写 SPI 桥(holder + 静态 vtable + trampoline) | 本教程第 7 章 |
| 7 | 安全层:RAII/错误/&mut/Send+Sync | 第 8 章 |
| 8 | 冒烟测试 + 联机测试 | 第 10 章 |
A.2 常用命令
powershell
# 查 DLL 导出符号
dumpbin /exports xtptraderapi.dll | findstr Create
# 跑全部测试(含需要 DLL 的)
$env:XTP_SDK_PATH="C:\path\to\xtp"; cargo test -- --include-ignored --test-threads=1
# 只跑静态测试(无 DLL)
cargo test
A.3 字段翻译口诀
char 数组用
[u8;N];枚举整型用 i32;typedef uint32 用 u32;指针入参看 const,非 const 用
&mut;union 先看判别字段;位域展开成单字节;
pack(1) 逐个验偏移;断言全用等值写。
附录 B 术语表
| 术语 | 解释 |
|---|---|
| ABI | 二进制接口约定:参数怎么传、栈谁清、符号叫什么、内存怎么排 |
| vtable | 虚函数表:C++ 对象前 8 字节指向的函数指针数组,多态的实现机制 |
| this 指针 | C++ 非静态成员函数的第一个隐藏参数,指向对象本身 |
| name mangling | 名字修饰:C++ 编译器把签名编码进符号名,如 ?CreateQuoteApi@... |
| SPI | Service Provider Interface,XTP 的回调接口类(QuoteSpi/TraderSpi) |
| trampoline | 跳板函数:C++ 调进来后转发到 Rust trait 方法的 extern "C" 函数 |
| UB | 未定义行为:如通过共享引用写内存、悬垂指针------不保证报错但随时爆炸 |
#[repr(C)] |
让 Rust 结构体按 C 规则布局(字段顺序、对齐) |
#[repr(C, packed)] |
C 布局 + 无填充,对应 #pragma pack(1) |
| FFI | Foreign Function Interface,跨语言调用接口 |
| RAII | 资源获取即初始化:用 Drop 自动释放 C++ 对象 |
| session_id | 交易登录成功后返回的会话编号,后续所有交易请求都要带 |
| request_id | 查询请求的编号,用来把查询应答回调和请求配对 |
第 12 章 连接、账号与生命周期(官方规则)
本章素材来自《API 常见问题》《XTP 行情服务接入前指引》《XTP 行情 Quote-API 断线后应对措施》《XTP 交易 Trader-API 使用示例说明》等官方文档。封装能编译只是起点,按官方规则正确使用才不会在生产环境翻车。
12.1 环境与连接方式速查
XTP 的行情有多种推送方式,选错连接方式是最常见的新手错误(收不到数据的第一嫌疑):
| 环境 | 连接方式 | 数据内容 | 关键限制 |
|---|---|---|---|
| 公网测试环境 | 只能 TCP | L1 级别:5 档快照、买一卖一队列、逐笔委托/成交,没有 OrderBook | 全体用户共享 2MB 带宽;单订阅接口单市场上限 100 只(沪深共 200);每日 23:30-00:10、8:00-9:00 维护 |
| 生产 L1 | TCP(以运营通知为准) | 5 档快照 | --- |
| 生产 L2 | 必须 UDP(组播) | 10 档快照、买一卖一队列、逐笔 | 组播可达性、缓冲区调优(见第 14 章) |
| 交易(任何环境) | 只能 TCP | --- | --- |
要点:
Login的sock_type参数必须与服务器推送方式一致,否则登录成功也收不到行情(官方 FAQ 84);- 公网测试环境不要用全市场订阅 (
SubscribeAllMarketData),带宽打满会被断线------用subscribe_market_data订几只股票即可(官方 FAQ 43、68、99); - 模拟测试环境的行情是 L1 五档,来自网页抓取(约 5 秒一次),与实盘有时间差;
- 交易服务器一般 8:45 开启(最晚 9:15),17:00 关闭;资金/持仓查询可用时段为 8:45-17:00。
#mermaid-svg-6HLVqghopMvbRdy0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6HLVqghopMvbRdy0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6HLVqghopMvbRdy0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6HLVqghopMvbRdy0 .error-icon{fill:#552222;}#mermaid-svg-6HLVqghopMvbRdy0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6HLVqghopMvbRdy0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6HLVqghopMvbRdy0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6HLVqghopMvbRdy0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6HLVqghopMvbRdy0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6HLVqghopMvbRdy0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6HLVqghopMvbRdy0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6HLVqghopMvbRdy0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6HLVqghopMvbRdy0 .marker.cross{stroke:#333333;}#mermaid-svg-6HLVqghopMvbRdy0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6HLVqghopMvbRdy0 p{margin:0;}#mermaid-svg-6HLVqghopMvbRdy0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6HLVqghopMvbRdy0 .cluster-label text{fill:#333;}#mermaid-svg-6HLVqghopMvbRdy0 .cluster-label span{color:#333;}#mermaid-svg-6HLVqghopMvbRdy0 .cluster-label span p{background-color:transparent;}#mermaid-svg-6HLVqghopMvbRdy0 .label text,#mermaid-svg-6HLVqghopMvbRdy0 span{fill:#333;color:#333;}#mermaid-svg-6HLVqghopMvbRdy0 .node rect,#mermaid-svg-6HLVqghopMvbRdy0 .node circle,#mermaid-svg-6HLVqghopMvbRdy0 .node ellipse,#mermaid-svg-6HLVqghopMvbRdy0 .node polygon,#mermaid-svg-6HLVqghopMvbRdy0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6HLVqghopMvbRdy0 .rough-node .label text,#mermaid-svg-6HLVqghopMvbRdy0 .node .label text,#mermaid-svg-6HLVqghopMvbRdy0 .image-shape .label,#mermaid-svg-6HLVqghopMvbRdy0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-6HLVqghopMvbRdy0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6HLVqghopMvbRdy0 .rough-node .label,#mermaid-svg-6HLVqghopMvbRdy0 .node .label,#mermaid-svg-6HLVqghopMvbRdy0 .image-shape .label,#mermaid-svg-6HLVqghopMvbRdy0 .icon-shape .label{text-align:center;}#mermaid-svg-6HLVqghopMvbRdy0 .node.clickable{cursor:pointer;}#mermaid-svg-6HLVqghopMvbRdy0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6HLVqghopMvbRdy0 .arrowheadPath{fill:#333333;}#mermaid-svg-6HLVqghopMvbRdy0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6HLVqghopMvbRdy0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6HLVqghopMvbRdy0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6HLVqghopMvbRdy0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6HLVqghopMvbRdy0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6HLVqghopMvbRdy0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6HLVqghopMvbRdy0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6HLVqghopMvbRdy0 .cluster text{fill:#333;}#mermaid-svg-6HLVqghopMvbRdy0 .cluster span{color:#333;}#mermaid-svg-6HLVqghopMvbRdy0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6HLVqghopMvbRdy0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6HLVqghopMvbRdy0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-6HLVqghopMvbRdy0 .icon-shape,#mermaid-svg-6HLVqghopMvbRdy0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6HLVqghopMvbRdy0 .icon-shape p,#mermaid-svg-6HLVqghopMvbRdy0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6HLVqghopMvbRdy0 .icon-shape .label rect,#mermaid-svg-6HLVqghopMvbRdy0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6HLVqghopMvbRdy0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6HLVqghopMvbRdy0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6HLVqghopMvbRdy0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 公网测试
生产 L1
生产 L2
交易
你的环境?
ProtocolType::Tcp
单订阅几只股票
ProtocolType::Tcp(以运营通知为准)
ProtocolType::Udp
- 组播调优(第 14 章)
ProtocolType::Tcp(唯一选择)
12.2 client_id 与实例规则(官方硬约束)
| 规则 | 内容 | 出处 |
|---|---|---|
| client_id 取值 | **199**;100255 是 XTP 预留 | Trader-API 示例说明 |
| client_id 数量 | 交易中建议不超过 5 个 ;超限报 used client_id number exceeded |
FAQ 6、71 |
| 同一 account + client_id | 同时只能有一个 session 连接,后者无法登录 | FAQ 4 |
| 多客户端登录同一账户 | 用不同 client_id 即可 | FAQ 6 |
| 一个进程内 TraderApi 数量 | 只能创建一个(多账户共用这一个实例,多次 Login 各自拿 session_id) | FAQ 11 |
| 一个进程内 QuoteApi 数量 | 2.2.33.5 起可多个,但只有第一次 Create 的参数有效,共用一份 quote.log | FAQ 11 |
| 过夜 | 不支持。策略程序必须每日重启:销毁 API → 次日重新 Create + Login | FAQ 12 |
| CreateApi 返回 NULL | 常见于:实例超上限、client_id 为 0、存储路径不存在、头文件与库版本不一致 | FAQ 104 |
对封装的启示:client_id 的类型是 u8(0-255),但合法业务区间是 1-99 。可以在 QuoteApi::new/TraderApi::new 里加一个防御性检查:
rust
if client_id == 0 || client_id > 99 {
return Err(XtpError::InvalidArgument(
"client_id 官方建议取值 1~99(0 和 100~255 均不合法)".into()));
}
12.3 同步 vs 异步:一张表记牢
| 接口 | 同步/异步 | 说明 |
|---|---|---|
Login |
同步阻塞 | 返回即知成败:行情返回 0 成功(-1 连接错、-2 已存在连接、-3 参数错);交易返回 session_id(>0 成功) |
Logout |
同步阻塞 | 不允许在回调线程调用 |
| 订阅/退订 | 异步 | 返回值只是"发送是否成功",结果看 OnSubMarketData 等回调 |
| 查询 | 异步 | 结果逐条 推送:N 条结果回调 N 次,最后一次 is_last = true |
InsertOrder/CancelOrder |
同步发送 | 返回值是 order_xtp_id(0 失败);订单状态看 OnOrderEvent |
GetApiLastError |
同步 | 非线程安全(官方 FAQ 185),其余接口都线程安全 |
查询回调里的两个高频误判:
rust
fn on_query_position(&self, pos: &XtpStkPosition, err: &XtpRspInfo, req: i32, is_last: bool, _: u64) {
// 误判 1:error_id 非 0 不一定是错误
if err.error_id == 11000350 {
return; // "Find none record" ------ 只是没有持仓,不是查询失败
}
// 误判 2:is_last=true 时数据依然有效(可能只有 error_info 有效,先看 error_id 再看数据)
if !err.is_ok() { return; }
// ... 处理 pos ...
if is_last { println!("本次查询完毕"); }
}
12.4 断线重连的正确姿势(交易)
官方推荐流程(FAQ 44、49):
OnDisconnected被触发时,不要 调Release()、不要 调Logout();- 直接在回调里(或通知主线程)循环调
Login(),失败间隔 ≥3 秒; - 重连成功后必须更新 session_id(重连后会变,FAQ 34、207);
- 未 Logout 的重连默认是 Resume 模式:从断点消息处续传,不会丢订单回报。
官方 C++ 模板翻译成 Rust(放在策略线程里跑,不要在回调线程里死循环):
rust
// 回调线程:只发通知
impl TraderSpi for MySpi {
fn on_disconnected(&self, session_id: u64, reason: i32) {
let _ = self.tx.send(Event::Disconnected { session_id, reason });
}
}
// 主循环:负责重连
fn run_reconnect_loop(api: &mut TraderApi, rx: &Receiver<Event>, cfg: &LoginCfg) {
while let Ok(ev) = rx.recv() {
if let Event::Disconnected { .. } = ev {
loop {
match api.login(&cfg.ip, cfg.port, &cfg.user, &cfg.pass, ProtocolType::Tcp, None) {
Ok(sid) if sid > 0 => {
println!("重连成功,新 session_id = {}", sid); // 已自动更新
break;
}
_ => std::thread::sleep(std::time::Duration::from_secs(10)), // 建议不小于 3s
}
}
}
}
}
12.5 公共流重传:Restart / Quick / Resume
SubscribePublicTopic() 必须在 Login 之前 调用,决定订单响应、成交回报这些"公共流"消息怎么补发:
| 方式 | 行为 | 用途 |
|---|---|---|
ResumeType::Restart (0) |
从本交易日开始重传全部公共流(OnOrderEvent/OnTradeEvent/OnFundTransfer) | 程序重启后恢复全量状态 |
ResumeType::Quick (1) |
只收登录之后的新消息 | 干净启动,最常用 |
| Resume(隐式) | 断线后不调 Logout 直接 Login,从断点续传 | 断线重连(见 12.4) |
注意官方 FAQ 20:断线后如果先 Logout 再 Login ,就按你设置的 Restart/Quick 来;如果不 Logout 直接 Login,默认 Resume 续传。两种路径收到的消息不同,别搞混。
12.6 行情断线的处理(Quote)
行情断线要分协议处理(官方《断线后应对措施》):
#mermaid-svg-5w6SIXa795s7o6hu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5w6SIXa795s7o6hu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5w6SIXa795s7o6hu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5w6SIXa795s7o6hu .error-icon{fill:#552222;}#mermaid-svg-5w6SIXa795s7o6hu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5w6SIXa795s7o6hu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5w6SIXa795s7o6hu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5w6SIXa795s7o6hu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5w6SIXa795s7o6hu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5w6SIXa795s7o6hu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5w6SIXa795s7o6hu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5w6SIXa795s7o6hu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5w6SIXa795s7o6hu .marker.cross{stroke:#333333;}#mermaid-svg-5w6SIXa795s7o6hu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5w6SIXa795s7o6hu p{margin:0;}#mermaid-svg-5w6SIXa795s7o6hu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5w6SIXa795s7o6hu .cluster-label text{fill:#333;}#mermaid-svg-5w6SIXa795s7o6hu .cluster-label span{color:#333;}#mermaid-svg-5w6SIXa795s7o6hu .cluster-label span p{background-color:transparent;}#mermaid-svg-5w6SIXa795s7o6hu .label text,#mermaid-svg-5w6SIXa795s7o6hu span{fill:#333;color:#333;}#mermaid-svg-5w6SIXa795s7o6hu .node rect,#mermaid-svg-5w6SIXa795s7o6hu .node circle,#mermaid-svg-5w6SIXa795s7o6hu .node ellipse,#mermaid-svg-5w6SIXa795s7o6hu .node polygon,#mermaid-svg-5w6SIXa795s7o6hu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5w6SIXa795s7o6hu .rough-node .label text,#mermaid-svg-5w6SIXa795s7o6hu .node .label text,#mermaid-svg-5w6SIXa795s7o6hu .image-shape .label,#mermaid-svg-5w6SIXa795s7o6hu .icon-shape .label{text-anchor:middle;}#mermaid-svg-5w6SIXa795s7o6hu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5w6SIXa795s7o6hu .rough-node .label,#mermaid-svg-5w6SIXa795s7o6hu .node .label,#mermaid-svg-5w6SIXa795s7o6hu .image-shape .label,#mermaid-svg-5w6SIXa795s7o6hu .icon-shape .label{text-align:center;}#mermaid-svg-5w6SIXa795s7o6hu .node.clickable{cursor:pointer;}#mermaid-svg-5w6SIXa795s7o6hu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5w6SIXa795s7o6hu .arrowheadPath{fill:#333333;}#mermaid-svg-5w6SIXa795s7o6hu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5w6SIXa795s7o6hu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5w6SIXa795s7o6hu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5w6SIXa795s7o6hu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5w6SIXa795s7o6hu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5w6SIXa795s7o6hu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5w6SIXa795s7o6hu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5w6SIXa795s7o6hu .cluster text{fill:#333;}#mermaid-svg-5w6SIXa795s7o6hu .cluster span{color:#333;}#mermaid-svg-5w6SIXa795s7o6hu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5w6SIXa795s7o6hu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5w6SIXa795s7o6hu rect.text{fill:none;stroke-width:0;}#mermaid-svg-5w6SIXa795s7o6hu .icon-shape,#mermaid-svg-5w6SIXa795s7o6hu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5w6SIXa795s7o6hu .icon-shape p,#mermaid-svg-5w6SIXa795s7o6hu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5w6SIXa795s7o6hu .icon-shape .label rect,#mermaid-svg-5w6SIXa795s7o6hu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5w6SIXa795s7o6hu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5w6SIXa795s7o6hu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5w6SIXa795s7o6hu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TCP(测试环境/L1)
UDP(生产 L2)
不需要
需要
是
OnDisconnected(reason) 触发
当前是哪种连接?
循环 Login(≥3s 间隔)
成功后重新订阅
TCP 不会自动重推行情
只是 TCP 通道断了
UDP 组播可能还在收行情
还需要查询静态数据吗?
可以不重连
重连 Login 即可
无需重新订阅
怀疑丢包?
RequestRebuildQuote 回补(见 14.6)
第 13 章 订单与查询的业务语义
13.1 订单生命周期与回调时序
XTP 订单的状态机与回调规则(官方 FAQ 14、33、101、173、242):
#mermaid-svg-CTxMFX1bjfFRKy8X{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CTxMFX1bjfFRKy8X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CTxMFX1bjfFRKy8X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CTxMFX1bjfFRKy8X .error-icon{fill:#552222;}#mermaid-svg-CTxMFX1bjfFRKy8X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CTxMFX1bjfFRKy8X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CTxMFX1bjfFRKy8X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CTxMFX1bjfFRKy8X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CTxMFX1bjfFRKy8X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CTxMFX1bjfFRKy8X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CTxMFX1bjfFRKy8X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CTxMFX1bjfFRKy8X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CTxMFX1bjfFRKy8X .marker.cross{stroke:#333333;}#mermaid-svg-CTxMFX1bjfFRKy8X svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CTxMFX1bjfFRKy8X p{margin:0;}#mermaid-svg-CTxMFX1bjfFRKy8X defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-CTxMFX1bjfFRKy8X g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-CTxMFX1bjfFRKy8X g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-CTxMFX1bjfFRKy8X g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-CTxMFX1bjfFRKy8X g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-CTxMFX1bjfFRKy8X g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-CTxMFX1bjfFRKy8X .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-CTxMFX1bjfFRKy8X .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-CTxMFX1bjfFRKy8X .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-CTxMFX1bjfFRKy8X .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-CTxMFX1bjfFRKy8X .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-CTxMFX1bjfFRKy8X .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-CTxMFX1bjfFRKy8X .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-CTxMFX1bjfFRKy8X .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CTxMFX1bjfFRKy8X .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CTxMFX1bjfFRKy8X .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CTxMFX1bjfFRKy8X .edgeLabel .label text{fill:#333;}#mermaid-svg-CTxMFX1bjfFRKy8X .label div .edgeLabel{color:#333;}#mermaid-svg-CTxMFX1bjfFRKy8X .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-CTxMFX1bjfFRKy8X .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-CTxMFX1bjfFRKy8X .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-CTxMFX1bjfFRKy8X .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-CTxMFX1bjfFRKy8X .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-CTxMFX1bjfFRKy8X .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CTxMFX1bjfFRKy8X .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CTxMFX1bjfFRKy8X #statediagram-barbEnd{fill:#333333;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CTxMFX1bjfFRKy8X .cluster-label,#mermaid-svg-CTxMFX1bjfFRKy8X .nodeLabel{color:#131300;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-CTxMFX1bjfFRKy8X .note-edge{stroke-dasharray:5;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagram-note text{fill:black;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagram-note .nodeLabel{color:black;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagram .edgeLabel{color:red;}#mermaid-svg-CTxMFX1bjfFRKy8X #dependencyStart,#mermaid-svg-CTxMFX1bjfFRKy8X #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-CTxMFX1bjfFRKy8X .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CTxMFX1bjfFRKy8X :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} InsertOrder 返回 order_xtp_id
OnOrderEvent(开始状态)
OnOrderEvent(终态)
OnOrderEvent(终态)
OnOrderEvent(终态)
OnOrderEvent(终态,带 error_info)
OnTradeEvent(部成不推 OnOrderEvent!)
本地初始
未成交
全部成交
部分撤单
已撤单
已拒绝
五条铁律:
- 部成不推
OnOrderEvent。要知道部成,只能累加OnTradeEvent的成交量,或调QueryOrdersEx查qty_left(FAQ 31); - 时序保证 :开始状态在所有成交回报之前 到达,结束状态在所有成交回报之后到达(FAQ 33);API 内部还有约 3~5 秒的保序窗口(FAQ 101);
InsertOrder还没返回,就可能先收到OnOrderEvent(FAQ 80/81)!此时回调里的order_xtp_id还没关联上你的本地订单------对策:- 用
order_client_id做关联键(回调原样带回); - 收到不认识的订单先缓存不要丢弃 ,等
InsertOrder返回后再对账; - 或者用
GetANewOrderXTPID()预取 id +InsertOrderExtra()报单(返回值即传入的 id,关联在发送前就建立);
- 用
qty_left的含义随状态变:未成交/全成/废单时是"未成交数量",部撤/全撤时是"被撤数量"(FAQ 24);- 撤单成功没有单独回调 ,原订单的
OnOrderEvent会变更为部撤/全撤;只有撤单失败才有OnCancelOrderError(FAQ 25)。
13.2 查询的正确姿势
- 查询结果逐条推送 ,N 条结果 N 次回调,
is_last=true收尾(FAQ 15);is_last那条也可能只带 error_info 没有数据(FAQ 234); error_id = 11000350("Find none record")表示没有记录,不是错误(FAQ 62);request_id自己管理,回调原样带回,用来把应答和请求配对;- 数据特别多时用分页查询 (
QueryOrdersByPage等):req_count每页条数、reference起始索引(首次传 0,之后传回调里的query_reference)、回调里order_sequence == req_count说明还有下一页(官方分页示例); - 不要轮询查询接口。官方明确"不建议轮询使用,当报单量过多时,容易造成用户线路拥堵,导致 api 断线"------要数据用订阅推送;
- 沪深两个市场的静态信息查询分开调 ,等一个市场全部回来(
is_last)再查另一个(官方 Quote-API 示例说明)。
13.3 时间与编码
| 字段 | 时间来源 |
|---|---|
行情 data_time |
交易所时间 |
交易 cancel_time、trade_time |
交易所时间 |
交易 insert_time、update_time |
XTP 本地时间 |
ticker_name 等字符串是 UTF-8,Rust 侧 from_cstring_bytes + to_string_lossy 直接正确;时间格式统一为 YYYYMMDDHHMMSSsss(i64)。
13.4 回调数据时效与线程模型(再强调)
- 回调参数指针只在回调期间有效 (FAQ 232):Rust 的
&T引用 + 需要时clone(),从类型系统上杜绝了 C++ 里"存指针后读脏数据"的经典坑; - 线程模型(FAQ 146-149):
- QuoteSpi:TCP = 1 个回调线程;UDP = 1 个 TCP 线程 + 每个组播组 1 个 UDP 线程(快照/逐笔/订单簿可能在不同线程);
- TraderSpi:查询回调 1 个线程;
OnOrderEvent/OnTradeEvent可能有 2 个线程(正常 + 超时线程); - 回补回调(
OnRebuildTickByTick等)与订阅回调不在同一个线程;
- 结论:同一批数据可能并发到达,用户状态要么
Mutex,要么无锁;这也是为什么 trait 必须Send + Sync。
第 14 章 行情数据语义与生产环境调优
14.1 ticker_status:8 字节交易状态解码
XtpMarketData.ticker_status: [u8; 8] 的每一位含义(官方《接入前指引》+ FAQ 64):
| 位 | 含义 | 取值 |
|---|---|---|
| 0 | 交易时段 | S启动(开市前) C集合竞价 T连续竞价 B休市( SZ) E闭市 P停牌 M可恢复熔断 N不可恢复熔断 U收盘集合竞价(SH) D集合竞价结束-连续竞价前(SH) A盘后交易(SZ) V波动性中断 |
| 1 | 可否正常交易 | 0不可 / 1可(无意义填空格) |
| 2 | 是否上市 | 0未上市 / 1已上市(深市无此位) |
| 3 | 当前时段是否接受报单 | 0不接受 / 1接受(深市无此位) |
判停牌的正确姿势(FAQ 229):看第 0 位是不是 P 不够,还要看第 1 位 ------停牌股交易所也可能发 T。债券例外:上交所 L2 债券的 ticker_status 无意义,要用 bond.instrument_status(FAQ 65)。
14.2 快照字段语义要点
| 字段 | 注意事项 |
|---|---|
last_price |
集合竞价阶段为 0(无成交) |
close_price |
上交易所有(>0 有效);深交所无此字段,XTP 赋值为 last_price |
qty |
单位:股票为股;债券行情里 SH 为手、SZ 为张(1 手=10 张);但交易报单统一为张 |
bid/ask 十档 |
L1 只有 5 档有效;期权永远只有 5 档;集合竞价阶段仅买一卖一有值 |
upper/lower_limit_price |
SZ 实时给出;SH 来自初始化文件(盘前可查,见 QueryAllTickers) |
trades_count |
SHL1 无意义(0),SHL2/SZ 有值 |
avg_price |
无意义字段 |
data_type_v2 |
0 指数 1 期权 2 现货 3 债券------读 union 前先看它 |
| 买一卖一队列 | 仅 L2 提供,数组最大 50 笔 ,实际长度看 bid1_count/ask1_count,9:25 后才有 |
14.3 逐笔(TickByTick)语义
channel_no:频道号。同一股票的逐笔委托与逐笔成交同频道;一个频道里有多只股票;seq在频道内连续(丢包检测依据)。SH 现有频道[1,6] [20] [801],SZ[2011-2015] [2021-2025] [2031-2035] [2061] [2071];- 字符型字段对照表:
| 字段 | SH | SZ |
|---|---|---|
| entrust.side | B买 S卖 |
1买 2卖 G借入 F出借 |
| entrust.ord_type | A增加 D删除(撤单在这里) |
1市价 2限价 U本方最优 |
| trade.trade_flag | B主动买 S主动卖 N未知(集合竞价都是 N) |
4撤单 F成交 |
- 委托↔成交追溯:SH 用
trade.bid_no/ask_no对entrust.order_no;SZ 对entrust.seq(FAQ 160); - 判断主动买卖:
bid_no > ask_no为主动买(买方后报单),反之为卖(FAQ 196); - 期权没有逐笔数据(FAQ 168)。
14.4 行情更新频率(心里有数)
| 品种 | 集合竞价 | 连续竞价 | 午间休市 |
|---|---|---|---|
| SH 指数 | 约 5 秒 | 约 5 秒 | 60 秒 |
| SH 股票 | 有变化 3 秒/无变化 60 秒 | 同左 | 60 秒 |
| SH 期权 | 有变化 15 秒/无变化 30 秒 | 有变化 0.5 秒/无变化 30 秒 | 30 秒 |
| SZ 指数 | 60 秒 | 3 秒 | 60 秒 |
| SZ 股票 | 60 秒 | 有变化 3 秒/无变化 60 秒 | 60 秒 |
| 逐笔/订单簿 | 实时 | 实时 | --- |
高峰时段(9:15、9:30、13:00、15:00)数据量约 20 万笔/秒 ,带宽约 22MB/秒------这就是为什么回调里什么都不许干。
14.5 UDP 生产环境调优清单
#mermaid-svg-vZqus6tBpb24WAKE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vZqus6tBpb24WAKE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vZqus6tBpb24WAKE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vZqus6tBpb24WAKE .error-icon{fill:#552222;}#mermaid-svg-vZqus6tBpb24WAKE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vZqus6tBpb24WAKE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vZqus6tBpb24WAKE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vZqus6tBpb24WAKE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vZqus6tBpb24WAKE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vZqus6tBpb24WAKE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vZqus6tBpb24WAKE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vZqus6tBpb24WAKE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vZqus6tBpb24WAKE .marker.cross{stroke:#333333;}#mermaid-svg-vZqus6tBpb24WAKE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vZqus6tBpb24WAKE p{margin:0;}#mermaid-svg-vZqus6tBpb24WAKE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vZqus6tBpb24WAKE .cluster-label text{fill:#333;}#mermaid-svg-vZqus6tBpb24WAKE .cluster-label span{color:#333;}#mermaid-svg-vZqus6tBpb24WAKE .cluster-label span p{background-color:transparent;}#mermaid-svg-vZqus6tBpb24WAKE .label text,#mermaid-svg-vZqus6tBpb24WAKE span{fill:#333;color:#333;}#mermaid-svg-vZqus6tBpb24WAKE .node rect,#mermaid-svg-vZqus6tBpb24WAKE .node circle,#mermaid-svg-vZqus6tBpb24WAKE .node ellipse,#mermaid-svg-vZqus6tBpb24WAKE .node polygon,#mermaid-svg-vZqus6tBpb24WAKE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vZqus6tBpb24WAKE .rough-node .label text,#mermaid-svg-vZqus6tBpb24WAKE .node .label text,#mermaid-svg-vZqus6tBpb24WAKE .image-shape .label,#mermaid-svg-vZqus6tBpb24WAKE .icon-shape .label{text-anchor:middle;}#mermaid-svg-vZqus6tBpb24WAKE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vZqus6tBpb24WAKE .rough-node .label,#mermaid-svg-vZqus6tBpb24WAKE .node .label,#mermaid-svg-vZqus6tBpb24WAKE .image-shape .label,#mermaid-svg-vZqus6tBpb24WAKE .icon-shape .label{text-align:center;}#mermaid-svg-vZqus6tBpb24WAKE .node.clickable{cursor:pointer;}#mermaid-svg-vZqus6tBpb24WAKE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vZqus6tBpb24WAKE .arrowheadPath{fill:#333333;}#mermaid-svg-vZqus6tBpb24WAKE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vZqus6tBpb24WAKE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vZqus6tBpb24WAKE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vZqus6tBpb24WAKE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vZqus6tBpb24WAKE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vZqus6tBpb24WAKE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vZqus6tBpb24WAKE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vZqus6tBpb24WAKE .cluster text{fill:#333;}#mermaid-svg-vZqus6tBpb24WAKE .cluster span{color:#333;}#mermaid-svg-vZqus6tBpb24WAKE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vZqus6tBpb24WAKE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vZqus6tBpb24WAKE rect.text{fill:none;stroke-width:0;}#mermaid-svg-vZqus6tBpb24WAKE .icon-shape,#mermaid-svg-vZqus6tBpb24WAKE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vZqus6tBpb24WAKE .icon-shape p,#mermaid-svg-vZqus6tBpb24WAKE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vZqus6tBpb24WAKE .icon-shape .label rect,#mermaid-svg-vZqus6tBpb24WAKE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vZqus6tBpb24WAKE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vZqus6tBpb24WAKE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vZqus6tBpb24WAKE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} UDP 收不到/丢包排查
- sock_type 必须是 Udp
- 防火墙关闭或放行组播
- Windows 注册表:IGMPVersion=3 / IGMPLevel=2
DefaultReceiveWindow=134217728(128M)
4. Linux:net.core.rmem_max=134217728
netstat -gn 确认组播组绑定到正确网卡
5. 缓冲区开大(见下)
6. 日志级别降到 INFO(DEBUG 太吵会丢包)
7. tcpdump/wireshark 确认组播包到没到机器
- 接收缓冲区 :经典版用
SetUDPBufferSize()(2 的次方 MB,官方建议 256~512MB,最低 64MB);XTPX 4.0 没有这个接口 ,改由quote_config.ini的L1_buf_capacity/L2_buf_capacity配置(见第 17 章); - 绑核 :XTPX 用
SetUDPThreadAffinityArray(必须在 SetConfigFile 之后、Login 之前调);经典版是SetUDPRecvThreadAffinityArray/SetUDPParseThreadAffinityArray分开绑。绑定靠后的核,绑第 1 个核容易和系统抢资源;绑的是逻辑 CPU,是"绑定"不是"隔离"(FAQ 53、131); - 异步日志 (udpseq 丢包排查日志):经典版用
SetUDPSeqLogOutPutFlag开关;XTPX 用CreateQuoteApi的第 4 个参数udpseq_output。调试期开,稳定后关(省一个核); - 丢包排查 :
quote.log里出现大量discrete关键字 = 丢包(启动时偶尔一条可忽略,那是订阅前服务器已发的包);udpseq0_0.*(快照 seq)、udpseq0_1.*(订单簿 seq)、udpseq0_2.*(逐笔 seq)、udpseq3_0.*(缓冲区满)分别检查; - 回调纪律 :TCP 下收得慢会被服务器断开 ;UDP 下收得慢会丢包。官方建议"生产者/消费者"模式(第 16 章实战)。
14.6 行情回补(RequestRebuildQuote)
UDP 丢包后,可以请求回补(官方《L2 行情数据回补功能的使用说明》):
rust
let req = XtpQuoteRebuildReq {
request_id: 1,
data_type: QuoteRebuildDataType::TickByTick as u32, // 1=快照 2=逐笔 3=指定股票逐笔
exchange_id: ExchangeType::SZ as u32,
channel_number: 2011, // 逐笔才用
_unuse: [0; 2],
ticker: [0; 16], // 快照/指定股票逐笔时用
begin: 20, // 逐笔:seq 起点(闭区间);快照:时间起点(闭区间)
end: 78, // 逐笔:seq 终点(闭区间);快照:时间终点(开区间)
};
api.request_rebuild_quote(&req)?;
// 数据从 on_rebuild_tick_by_tick / on_rebuild_market_data 回调(与订阅回调不同线程)
// 结果从 on_request_rebuild_quote 回调:result_code=PARTLY(2) 说明没补完,要接着请求
要点:一次最多回补 1000 条,超了分批;result_code 为 NO_DATA(3) 可能服务器也缺数据,等会儿再试;FREQUENTLY(5) 是被限频,降速。XTPX 4.0 的回补集成在主 API 里 (无需单独登录);经典版要 LoginToRebuildQuoteServer 单独连回补服务器,用后及时断开(第 17 章详述)。
第 15 章 风控、错误代码与下单参数速查
15.1 风控规则(别把你的策略打成筛子)
交易服务器有多层风控(官方《XTP 风控规则说明》),触发后限开仓甚至断线,回滚/熔断时间各自独立:
| 规则 | 作用 |
|---|---|
| Rule1/2 | 单笔订单股数/金额上限(防乌龙指) |
| Rule9/10 | 限价单/市价单股数上限 |
| Rule14 | 单位时间(1 秒)订单次数上限,触发"熔断"(一段时间内继续报违规) |
| Rule40 | 单位时间单连接 订单+撤单次数上限,触发直接断线 |
| Rule25/26/27/38 | 被市场/柜台拒绝次数累计或周期上限,超限拦开仓单 |
| Rule41 | 累计撤单次数上限 |
| Rule42 | 撤单/订单比上限(撤单比例太高拦开仓单) |
| Rule43 | 单位时间单只股票订单次数上限 |
| Rule44 | 委托订单最小金额下限 |
| 默认 1/2/3 | 可用资金不足 / 可用仓位不足 / 超涨跌停报价(永远生效) |
相关错误码:11000450(订单频率被限)、11000451(账户被限制交易)、11000301(风控拒绝)。策略要做好限流(每秒报单/撤单计数),触发风控后当天基本只能等第二天(FAQ 36:模拟环境触发风控要等服务器重启)。
15.2 模拟撮合:用 order_client_id 控制撮合结果
公网测试环境默认轮询撮合(未成交/部成/全成/拒单轮流)。API 下单时可用 order_client_id 指定(FAQ 36):
| order_client_id | 撮合结果 |
|---|---|
| 1 | 未成交 |
| 2 | 全部成交(单笔回报) |
| 3 | 部分成交 |
| 4 | 废单 |
| 5 | 全部成交(多笔回报) |
| 6 | 按当前快照盘口撮合(主板/科创/创业/ETF 买卖支持;债券仅上海) |
模拟交易所的拒单 error_code 为 11110000/11100000(msg 为 217/10000/29999)是模拟拒单,方便你测试拒单处理路径,不代表报单有错。
15.3 常用错误代码速查
完整表见附录 C。使用建议:程序里对 error_id 做分类 处理------11000350 当"空结果"而非错误;10200000/10210000 当"网络/服务器未就绪"走重连;11000030/11000033 当"账号/密钥错误"直接报警终止。
第 16 章 综合实战:最小可用行情接收器
把前面所有原则落成一个能跑的生产者/消费者行情接收器:回调线程只负责"拷贝数据、扔进 channel",处理线程负责业务。这是官方反复推荐的模式。
rust
//! market_data_receiver.rs ------ 生产者/消费者模式行情接收器
use std::sync::mpsc::{channel, Receiver, Sender};
use xtp_api::common::*;
use xtp_api::quote::*;
/// 传给处理线程的消息(拥有所有权,不依赖回调内存)
#[derive(Debug)]
enum MdEvent {
Snapshot(Box<XtpMarketData>),
TickByTick(Box<XtpTickByTick>),
Disconnected(i32),
SubAck { ticker: String, ok: bool, is_last: bool },
}
struct SpiImpl { tx: Sender<MdEvent> }
impl QuoteSpi for SpiImpl {
// 军规:回调里只做最轻的事 ------ clone 数据 + send,立刻返回
fn on_depth_market_data(&self, md: &XtpMarketData, _: &[i64], _: i32, _: i32, _: &[i64], _: i32, _: i32) {
let _ = self.tx.send(MdEvent::Snapshot(Box::new(*md)));
}
fn on_tick_by_tick(&self, tbt: &XtpTickByTick) {
let _ = self.tx.send(MdEvent::TickByTick(Box::new(*tbt)));
}
fn on_disconnected(&self, reason: i32) {
let _ = self.tx.send(MdEvent::Disconnected(reason));
}
fn on_sub_market_data(&self, t: &XtpSpecificTicker, e: &XtpRspInfo, is_last: bool) {
let _ = self.tx.send(MdEvent::SubAck { ticker: t.ticker_str(), ok: e.is_ok(), is_last });
}
}
fn consumer_loop(rx: Receiver<MdEvent>) {
let mut snaps = 0u64;
let mut tbts = 0u64;
let mut last = std::time::Instant::now();
while let Ok(ev) = rx.recv() {
match ev {
MdEvent::Snapshot(md) => {
snaps += 1;
// 业务处理在这里:策略信号、落盘、转发...
if snaps % 1000 == 1 {
println!("快照 {} | {} last={:.2} vol={}", snaps, md.ticker_str(), md.last_price, md.qty);
}
}
MdEvent::TickByTick(_) => tbts += 1,
MdEvent::Disconnected(r) => {
eprintln!("[!] 断线 reason={},由外层负责重连(见 12.6)", r);
}
MdEvent::SubAck { ticker, ok, is_last } => {
println!("订阅{}: {} {}", if ok { "成功" } else { "失败" }, ticker, if is_last { "(最后一条)" } else { "" });
}
}
if last.elapsed().as_secs() >= 10 {
println!("-- 统计:快照 {} 条,逐笔 {} 条 --", snaps, tbts);
last = std::time::Instant::now();
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let (tx, rx) = channel::<MdEvent>();
std::thread::spawn(move || consumer_loop(rx));
let user = std::env::var("XTP_QUOTE_USER")?;
let pass = std::env::var("XTP_QUOTE_PASSWORD")?;
let mut api = QuoteApi::new(1, "./data", LogLevel::Info, true)?; // 生产用 Info,别用 Debug
api.set_config_file("./quote_config.ini")?;
api.register_spi(Box::new(SpiImpl { tx }));
api.login(&std::env::var("XTP_QUOTE_IP")?, 6002, &user, &pass, ProtocolType::Tcp, None)?;
// 公网测试环境:只订少量股票(共享 2MB 带宽!)
api.subscribe_market_data(&["600000", "600519"], ExchangeType::SH)?;
api.subscribe_tick_by_tick(&["000001"], ExchangeType::SZ)?;
println!("接收中,Ctrl+C 退出");
loop { std::thread::sleep(std::time::Duration::from_secs(60)); }
}
对照检查清单(这个例子做对了什么):
- 回调里零格式化、零锁竞争,只
clone + send; - 日志级别用
Info而不是Debug(DEBUG 日志量大到能丢包); - 测试环境单订阅少量股票,不碰全市场订阅;
- 消息拥有所有权(
Box<XtpMarketData>),不依赖回调内存时效; - 断线只发事件,重连逻辑放外层(回调线程不死循环)。
第 17 章 XTPX 4.0 与经典版 API 的差异
这是最容易被忽视、后果最严重的一章。中泰有两代行情 API,函数名大量雷同,但 ABI 细节不同。封装错版本 = 全部白干。
17.1 差异总表
| 维度 | 经典版 Quote API | XTPX 4.0 Quote API(本教程封装对象) |
|---|---|---|
| DLL | xtpquoteapi.dll(随交易包分发) |
xtpxquoteapi.dll(独立行情包) |
| 命名空间 | XTP::API |
XTPX::API |
| 工厂函数 | CreateQuoteApi(client_id, path, log_level) 3 参数 |
CreateQuoteApi(client_id, path, log_level, udpseq_output) 4 参数 |
| 行情配置文件 | 无 | SetConfigFile(quote_config.ini)(登录前必须设置,否则收不到行情) |
| UDP 缓冲区 | SetUDPBufferSize(MB) |
无此接口 ,由 quote_config.ini 的 L1/L2_buf_capacity 决定 |
| UDP 异步日志开关 | SetUDPSeqLogOutPutFlag(bool) |
工厂函数第 4 参 udpseq_output |
| 线程绑核 | SetUDPRecvThreadAffinityArray + SetUDPParseThreadAffinityArray(两个) |
SetUDPThreadAffinityArray(一个) |
| 行情回补 | LoginToRebuildQuoteServer/LogoutFromRebuildQuoteServer 单独登录回补服务器 |
集成进主 API :RequestRebuildQuote 直接调 |
| 回补断线回调 | OnRebuildQuoteServerDisconnected |
无(并入主连接) |
XTP_EXCHANGE_TYPE |
enum:SH=1,SZ=2,NQ=3,UNKNOWN=4 | typedef uint32_t:SH=1,SZ=2,NQ=3,HK=4,UNKNOWN=5 |
XTPMarketDataStruct 布局 |
exchange_id 在 ticker 之前 ;data_type/data_type_v2 在结构体末尾 |
ticker 在前,exchange_id、data_type_v2 紧随其后(见第 5 章) |
| 新三板静态信息 | XTPNQFI |
XTPQuoteNQFullInfo(位域布局见 5.4) |
| 港股通/指数通 | 无 | 有(SubscribeAllHKCMarketData/SubscribeAllIndexPress 等) |
| 逐笔状态订单 | 2.2.32 起新增 | 同左(TbtType::State=3) |
| pack 值 | 头文件 #pragma pack(8) |
头文件 #pragma pack(1) |
17.2 quote_config.ini 详解(XTPX 特有)
XTPX 4.0 把 UDP 接收的各项参数从代码挪到了配置文件(官方要求绝对路径 ,必须在 Login 前 set_config_file):
ini
[md] ; 快照(MarketData)通道
decode_flag = 1
parse_cpu_id = 2 ; 解析线程绑核
[md.normal]
enable = ON
local_ip = 127.0.0.1 ; 接收网卡(生产环境填实际网卡 IP)
recv_cpu_id = 3 ; 接收线程绑核
enable_efvi = OFF ; solarflare 网卡加速
L1_buf_capacity = 256 ; L1 接收缓冲(MB)
L2_buf_capacity = 8 ; L2 接收缓冲(MB)
busy_wait = ON ; 忙等模式(低延迟,费 CPU)
[tbt] ... ; 逐笔通道(同上结构)
[ob] ... ; 订单簿通道(同上结构)
[subscribe_quote_type]; 各品种行情源开关(ON/OFF)
sh_level2_md_stock = ON
sz_level2_tbt_stock = ON
...
SetUDPThreadAffinityArray 的作用是把 md/ob/tbt 里 enable=ON 的通道按数组顺序重新分配 CPU(配置文件中 enable=OFF 的不会分配)。这与第 8 章修复的接口一一对应。
17.3 给封装者的警告
- 先确认目标 DLL 属于哪一代 ,再选头文件。
GetApiVersion()返回形如1.2.1-r.3的是 XTPX;形如2.2.x.x的是经典版; - 两个包的
XTP_EXCHANGE_TYPE枚举末位不同(UNKNOWN=4 vs 5,且 XTPX 多了 HK=4)------全订阅传 UNKNOWN 时,传错值行为全变; - 两个包的
XTPMarketDataStruct布局不同(经典版 exchange_id 在前且判别字段在末尾)。网上(包括官方部分老文档)流传的结构图是经典版的,照抄必错; - 经典版文档里的
SetUDPBufferSize、LoginToRebuildQuoteServer在 XTPX 里不存在,vtable 里找不到对应槽位; - 同一个进程可以同时使用交易 DLL + XTPX 行情 DLL(本教程的组合),也可以交易 DLL + 经典行情 DLL(官方 demo 的组合)------但别在同一个类里混用两代行情接口。
附录 C 常用错误代码表
完整表见官方《XTP 错误代码速查表》。按"你在什么时候会遇到"分类整理。
连接与登录(102xxxxx)
| 代码 | 含义 | 对策 |
|---|---|---|
| 10200000 | 行情服务器未开启/无网络连接 | 检查 IP/端口/防火墙,走重连 |
| 10200004 | 重复连接(已登录) | 先 Logout 或用已有连接 |
| 10200101 | CreateQuoteApi 参数错(client_id=0 或路径错) | 检查构造参数 |
| 10210000 | 交易服务器未开启/无网络连接 | 同上 |
| 10210101 | CreateTraderApi 参数错 | 同上 |
账号与权限(11000xxx)
| 代码 | 含义 | 对策 |
|---|---|---|
| 11000030 | 用户名或密码不正确 | 报警终止,检查凭据 |
| 11000033 | 交易密钥错误或未设置 | 检查 SetSoftwareKey 是否在 Login 前调用且 key 正确 |
| 11000043/44/45 | client_id 使用个数超限 | 固定使用 1~99 内的少量 client_id |
| 11000311 | 未在对应节点交易对应市场(一账号两中心) | 检查连接的节点与报单市场 |
| 11000550~11000573 | 未开通某权限(风险警示板/市价单/创业板/科创板等) | 联系营业部开通 |
报单参数(11001xxx / 110003xx)
| 代码 | 含义 | 对策 |
|---|---|---|
| 11000107 | 无效订单数量(超 100 万股/零散股规则) | 检查 quantity 规则(见附录 D) |
| 11000109 | 无效价格类型 | 检查 price_type 与市场/品种匹配 |
| 11000301 | 风控拒绝 | 见 15.1 |
| 11000305 | 找不到原始订单(撤单 id 错) | 检查传入的 order_xtp_id |
| 11000306 | 业务尚未支持 | 换业务类型 |
| 11000308/09/10 | 业务类型与证券/方向/用户类型不匹配 | 对照附录 D 检查 business_type/side |
| 11000350 | 未发现记录 | 不是错误,是空结果 |
| 11000381/11000384 | 资金余额不足/可划转资金不足 | 查资金再下单 |
| 11000450 | 报单频率被限 | 限流,见 15.1 |
查询与行情(11210xxx 等)
| 代码 | 含义 | 对策 |
|---|---|---|
| 11210100 | tickers or sessions used up(订阅超限) | 测试环境单市场 ≤100 只 |
| 11000390 | 获取 ETF 基本信息失败 | 该 ETF 不支持申赎或市场填错 |
| 11100055 | Tgw of the pbu id not found | 实盘找营业部查账户配置;测试环境查代码/市场 |
附录 D 下单参数组合速查表
灰色参数(不需要的)一律置 0。
session_id为登录返回值。
表 D-1 现货与常见业务
| 业务 | market | price_type | side | position_effect | business_type | 备注 |
|---|---|---|---|---|---|---|
| 买入股票 | SHA/SZA/BJA | 按注释选(1~8) | 1 BUY | 0 | 0 CASH | 普通股票 100 股整数倍;科创板 ≥200 股起、1 股递增 |
| 卖出股票 | 同上 | 同上 | 2 SELL | 0 | 0 CASH | 不超过 sellable_qty;零散股一次性卖出 |
| 撤单 | --- | --- | --- | --- | --- | cancel_order(order_xtp_id) |
| 新股申购 | SHA/SZA | 1 LIMIT | 1 BUY | 0 | 1 IPOS | 数量 ≤ min(可申购额度, 最大允许申购数量) |
| 新债申购 | 同上 | 1 LIMIT | 1 BUY | 0 | 1 IPOS | 同上 |
| 配股 | 对应市场 | 1 LIMIT | 1 BUY | 0 | 6 ALLOTMENT | 配债同(business_type=6) |
| 国债逆回购 | SHA/SZA | 1 LIMIT | 2 SELL | 0 | 2 REPO | quantity 单位张(1000 元面额=10 张=1 手),1000 元面额整数倍 |
| ETF 申购 | 对应市场 | 1 LIMIT | 7 PURCHASE | 0 | 3 ETF | 仅现货账户支持;二级市场代码 |
| ETF 赎回 | 对应市场 | 1 LIMIT | 8 REDEMPTION | 0 | 3 ETF | 当日申购的份额当日不可赎回(可卖出) |
表 D-2 两融与期权业务
| 业务 | market | price_type | side | position_effect | business_type | 备注 |
|---|---|---|---|---|---|---|
| 担保品买(两融) | 对应市场 | 1 LIMIT | 1 BUY | 0 | 4 MARGIN | |
| 担保品卖(两融) | 对应市场 | 1 LIMIT | 2 SELL | 0 | 4 MARGIN | |
| 融资买入 | 对应市场 | 1 LIMIT | 21 MARGIN_TRADE | 0 | 4 MARGIN | 标的两融标的 |
| 融券卖出 | 对应市场 | 1 LIMIT | 22 SHORT_SELL | 0 | 4 MARGIN | 同上 |
| 卖券还款 | 对应市场 | 1 LIMIT | 23 REPAY_MARGIN | 0 | 4 MARGIN | |
| 买券还券 | 对应市场 | 1 LIMIT | 24 REPAY_STOCK | 0 | 4 MARGIN | |
| 现券还券 | 对应市场 | 1 LIMIT | 26 STOCK_REPAY_STOCK | 0 | 4 MARGIN | 无成交回报,看订单确认 |
| 担保品转入 | 对应市场 | 1 LIMIT | 28 GRTSTK_TRANSIN | 0 | 4 MARGIN | 普通户→信用户,信用户登录操作 |
| 担保品转出 | 对应市场 | 1 LIMIT | 29 GRTSTK_TRANSOUT | 0 | 4 MARGIN | 维持担保比例 300% 以上部分可转 |
| 期权开/平仓 | 对应市场 | 期权可用类型 | 1/2 | 1 OPEN/2 CLOSE | 10 OPTION | position_effect 仅期权有效 |
报单被拒自查清单(官方 FAQ 30):数量规则(100 整数倍/科创板 200 起/逆回购面额)→ 价格是否超涨跌停 → price_type 是否被该品种支持(可转债只支持 LIMIT)→ ticker 与 market 是否匹配 → business_type 是否正确(信用账户普通买卖用 MARGIN + 担保品方向)→ position_effect 非期权填 0 → 是否触发风控(频率/撤单比)→ 是否在可报单时段(8:45 前报单只是缓存,9:15 才发往交易所)。
附录 E 官方文档索引
在线资源
| 资源 | 地址 |
|---|---|
| XTP 官方文档 | https://xtp.zts.com.cn/doc/api/xtpDoc |
| API 下载 | https://xtp.zts.com.cn/service/download |
| Python 接口 | https://github.com/ztsec/xtp_api_python |
| Java 接口 | https://github.com/ztsec/xtp_api_java |
| 错误代码速查 | 官网文档内"错误代码速查表";交易所拒单见 IS111(上交所)/深交所 Binary 接口规范 |
本地官方文档包(xtp_c++)导读
| 文档 | 什么时候读 |
|---|---|
头文件.md / 类库文件.md |
开始封装前:确认头文件清单与各平台库文件 |
QuoteApi.md / quoteSpi.md / TraderApi.md / TraderSpi.md |
逐函数翻译 vtable 与 SPI 时(与头文件对照) |
XTP行情服务接入前指引.md |
接行情前必读:字段语义、更新频率、丢包排查 |
XTP行情Quote-API使用示例说明.md |
行情 QuickStart 与断线重连模板 |
XTP行情Quote-API断线后应对措施.md |
TCP/UDP 断线策略 + 分页查询示例 |
XTP交易Trader-API使用示例说明.md |
交易 QuickStart、client_id/实例/过夜规则 |
XTP交易业务报单接口参数说明.md |
每种业务的下单参数组合(附录 D 的来源) |
API常见问题.md |
130+ 条 FAQ,出问题先搜它 |
XTP错误代码速查表.md |
收到陌生 error_id 时查 |
XTP风控规则说明.md |
写限流逻辑前读 |
XTP关于L2行情数据回补功能的使用说明.md |
用回补接口前读(注意是经典版接口) |
XTP模拟测试环境常见问题.md |
用测试环境前读(撮合规则/时段/限制) |
版本更新介绍.md |
升级 API 版本时读,关注结构体/枚举变更 |
新股/新债/配股/资金划拨/期逻辑示例代码.md |
做对应业务时参考参数填法 |
结语 :FFI 封装的功夫不在 Rust 语法,而在对 C++ 内存模型的精确理解 和用测试锁死每一条假设的纪律。前 11 章解决"怎么封装"------头文件是唯一合同、布局要手工验算、断言必须等值、回调注意安全;第 12~17 章解决"怎么用对"------连接方式别选错、client_id 别乱用、回调里别干活、生产环境要调优。两者兼备,你就有能力封装并投产任何 C++ 交易接口了。祝实习顺利,代码无 UB。