Kimi K3:Rust封装XTP接口详细教程实践

用 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.hxtpx_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 里使用它,并且做到:

  1. 调得动:创建 API、登录、订阅、查询、下单;
  2. 收得到 :行情推送、报单回报等回调能进 Rust 代码;
  3. 不崩溃、不 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();

注意时序约束 :RegisterSpiSetHeartBeatIntervalSetSoftwareKey 等都必须 在 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
⑤ 起:栈上

要点:

  1. Rust 的 extern "C" fn 在 x86_64-pc-windows-msvc 目标上就是这套约定;

  2. C++ 非静态成员函数 会把 this 当作第一个隐藏参数传入 RCX。所以:

    cpp 复制代码
    obj->Login(ip, port)     // C++ 写法
    // 等价于 C 写法:
    TraderApi_Login(obj, ip, port)   // this 成了第一个参数
  3. 成员函数与 extern "C" fn(this: *mut c_void, ...) 参数逐一对齐后,可以直接互调;

  4. bool 是 1 字节,C++ bool 与 Rust bool 兼容;

  5. 超过 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,没有偏移。

我们只要:

  1. 读出对象前 8 字节得到 vtable 地址;
  2. 把 vtable 解释成一个"函数指针数组";
  3. 用对应槽位的函数指针,以 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.rsC:\Users\xxx\Desktop\xtp 这种个人绝对路径XTP_API_20250806_2.2.50.8 这种带版本号的目录名 硬编码进去,换台机器就废。规则:只用环境变量,且拷贝前校验目标 DLL 真实存在


第 4 章 重建 vtable:调用第一个 C++ 虚函数

4.1 把 vtable 声明成一个 Rust 结构体

对着 xtpx_quote_api.hQuoteApi 的虚函数声明顺序 ,写一个 #[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,
}

规则:

  1. #[repr(C)] 必须加,否则 Rust 自由重排字段;
  2. char[N][u8; N],不要String/CString(它们是指针,布局完全不同);
  3. 定长数组、嵌套结构体、union 全部按同样规则递归翻译;
  4. 字符串读取另写辅助函数,字段本身保持字节数组。
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_ofoffset_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;
// ...

逐个数字核对! 真实案例:FundTransferTypeUnknown 在 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: &QUOTE_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 回调里的工程纪律(写给使用者)

封装文档里必须警告用户:

  1. 回调在 DLL 线程里跑,必须快速返回 。要处理数据就 channel.send() 丢给自己的线程。官方 FAQ 明确:回调处理过慢会填满接收缓冲区,服务器会断开连接(UDP 下则直接丢包);
  2. 回调里不要 再调同一 API 的阻塞函数(可能死锁),官方文档注明 Logout 不允许在回调线程调;
  3. 回调参数(&XtpMarketData 等)只在调用期间有效------官方 FAQ 232 条明确警告"只保存指针不拷贝数据,之后读到的是被复用的内存"。Rust 的 &T + clone 在类型层面就强制了这一点(C++ 里靠自觉);
  4. 断线回调 on_disconnected 里可以重新 Login(官方推荐做法,见第 12 章),但不要 在回调里调 ReleaseLogout;
  5. 屏幕输出(println!)是耗时操作,行情高峰(每秒约 20 万笔)时千万别在回调里逐条打印------官方 FAQ 58 条把"有屏幕输出"列为全订阅断线的首要嫌疑;
  6. 回调线程不止一个 (官方 FAQ 146-149):QuoteSpi 在 TCP 下是 1 个线程,UDP 下是 1 个 TCP 线程 + 每组播组 1 个 UDP 线程;TraderSpi 的查询回调是 1 个线程,但 OnOrderEvent/OnTradeEvent 可能由正常线程 + 超时线程 两个线程回调。所以 trait 要求 Send + Sync,用户实现里的共享状态必须自己加锁(Mutex)或用无锁结构;
  7. XTP 字符串(ticker_name 等)是 UTF-8 编码 。Rust String 天然 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 总是先登录所以不触发。对策:

  1. 封装文档里写明:创建后请先 Login 再 Release;
  2. 测试用例按"创建 → 尝试登录 → 释放"的顺序写;
  3. 交易 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 的错
崩溃

  1. 最小复现

写一个 20 行的 main 单独跑
2. 加无缓冲日志 eprintln!

定位崩在哪一行
3. 二分变量

mem::forget 跳过 Release?

不加载 SPI?
4. 对照实验

官方 C++ demo 同操作崩不崩?
结论
修布局/签名/生命周期
记录怪癖 + 规避路径

复盘我们遇到的崩溃:

  1. 现象:创建 QuoteApi → 打印版本(成功)→ drop 时崩;
  2. mem::forget(不 Release):不崩 ⇒ 问题在 Release 或 DLL 卸载;
  3. 显式 release() 后立刻崩 ⇒ Release 内部崩;
  4. 官方 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 --- ---

要点:

  • Loginsock_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):

  1. OnDisconnected 被触发时,不要Release()不要Logout();
  2. 直接在回调里(或通知主线程)循环调 Login(),失败间隔 ≥3 秒;
  3. 重连成功后必须更新 session_id(重连后会变,FAQ 34、207);
  4. 未 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!)
本地初始
未成交
全部成交
部分撤单
已撤单
已拒绝

五条铁律:

  1. 部成不推 OnOrderEvent 。要知道部成,只能累加 OnTradeEvent 的成交量,或调 QueryOrdersExqty_left(FAQ 31);
  2. 时序保证 :开始状态在所有成交回报之前 到达,结束状态在所有成交回报之后到达(FAQ 33);API 内部还有约 3~5 秒的保序窗口(FAQ 101);
  3. InsertOrder 还没返回,就可能先收到 OnOrderEvent (FAQ 80/81)!此时回调里的 order_xtp_id 还没关联上你的本地订单------对策:
    • order_client_id 做关联键(回调原样带回);
    • 收到不认识的订单先缓存不要丢弃 ,等 InsertOrder 返回后再对账;
    • 或者用 GetANewOrderXTPID() 预取 id + InsertOrderExtra() 报单(返回值即传入的 id,关联在发送前就建立);
  4. qty_left 的含义随状态变:未成交/全成/废单时是"未成交数量",部撤/全撤时是"被撤数量"(FAQ 24);
  5. 撤单成功没有单独回调 ,原订单的 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_timetrade_time 交易所时间
交易 insert_timeupdate_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 BS 12G借入 F出借
entrust.ord_type A增加 D删除(撤单在这里) 1市价 2限价 U本方最优
trade.trade_flag B主动买 S主动卖 N未知(集合竞价都是 N) 4撤单 F成交
  • 委托↔成交追溯:SH 用 trade.bid_no/ask_noentrust.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 收不到/丢包排查

  1. sock_type 必须是 Udp
  2. 防火墙关闭或放行组播
  3. 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.iniL1_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_codeNO_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.iniL1/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_idticker 之前 ;data_type/data_type_v2 在结构体末尾 ticker 在前,exchange_iddata_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/tbtenable=ON 的通道按数组顺序重新分配 CPU(配置文件中 enable=OFF 的不会分配)。这与第 8 章修复的接口一一对应。

17.3 给封装者的警告

  1. 先确认目标 DLL 属于哪一代 ,再选头文件。GetApiVersion() 返回形如 1.2.1-r.3 的是 XTPX;形如 2.2.x.x 的是经典版;
  2. 两个包的 XTP_EXCHANGE_TYPE 枚举末位不同(UNKNOWN=4 vs 5,且 XTPX 多了 HK=4)------全订阅传 UNKNOWN 时,传错值行为全变;
  3. 两个包的 XTPMarketDataStruct 布局不同(经典版 exchange_id 在前且判别字段在末尾)。网上(包括官方部分老文档)流传的结构图是经典版的,照抄必错;
  4. 经典版文档里的 SetUDPBufferSizeLoginToRebuildQuoteServer 在 XTPX 里不存在,vtable 里找不到对应槽位;
  5. 同一个进程可以同时使用交易 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。

相关推荐
kebeiovo5 小时前
游戏服务端开发:Actor模型详解(Go语言)
开发语言·后端·golang
红烧大青虫6 小时前
setInterval 倒计时实现:60s 验证码发送逻辑
后端·华为·harmonyos·鸿蒙系统
迷途呀6 小时前
Python:函数中的参数类型
开发语言·笔记·python·langchain·nlp
Herbert_hwt6 小时前
建立Java程序开发
java·开发语言
小村儿6 小时前
连载14-实战篇--一个半月,我一个人和 Claude Code 搭出一套数字人工程
前端·后端·ai编程
你驴我6 小时前
WhatsApp 多账号下消息已读回执的实时聚合与推送实践
后端·python
用户208046804566 小时前
Python3 注释编写完全指南:从基础规范到高效实践
后端
愚公移码6 小时前
蓝凌EKP18产品:流程虚拟机(PVM)
java·开发语言·前端
苏三说技术7 小时前
Jackson3来了,变化真大!
后端