前言
网上有不少教程讲到 CTP API 的编译,但是我按照多数教程照着做都不太成功,只有一份是成功了。在此把过程记录下来,希望可以帮到更多的人。
(2022年4月更新:补充 Windows 下的支持,写在文末)
Linux
1. 下载 CTP API
在上期的官网下载,然后解压到某一个工作目录。我们只需要 Linux x64 的,工作目录的文件如下:
error.dtd error.xml ThostFtdcMdApi.h ThostFtdcTraderApi.h ThostFtdcUserApiDataType.h ThostFtdcUserApiStruct.h thostmduserapi_se.so thosttraderapi_se.so
2. 安装 SWIG
SWIG 是一个能将 C/C++ 接口转换为其他语言的工具,支持 Python, Java, C#, Go 等多种编程语言。
我这里直接安装系统源提供的版本:sudo apt install swig,我装的是 3.0.12 版本。如果是 Windows 系统,请自行到官网下载安装。
3. 使用 SWIG 得到 Java 包
一、接口文件 thosttraderapi.i
在工作目录创建文件 thosttraderapi.i,内容如下:
%module(directors="1") thosttradeapi
%{
#include "ThostFtdcTraderApi.h"
#include "iconv.h"
%}
%typemap(out) char[ANY], char[] {
if ($1) {
iconv_t cd = iconv_open("utf-8", "gb2312");
if (cd != reinterpret_cast<iconv_t>(-1)) {
char buf[4096] = {};
char **in = &$1;
char *out = buf;
size_t inlen = strlen($1), outlen = 4096;
if (iconv(cd, in, &inlen, &out, &outlen) != static_cast<size_t>(-1))
$result = JCALL1(NewStringUTF, jenv, (const char *)buf);
iconv_close(cd);
}
}
}
%feature("director") CThostFtdcTraderSpi;
%ignore THOST_FTDC_VTC_BankBankToFuture;
%ignore THOST_FTDC_VTC_BankFutureToBank;
%ignore THOST_FTDC_VTC_FutureBankToFuture;
%ignore THOST_FTDC_VTC_FutureFutureToBank;
%ignore THOST_FTDC_FTC_BankLaunchBankToBroker;
%ignore THOST_FTDC_FTC_BrokerLaunchBankToBroker;
%ignore THOST_FTDC_FTC_BankLaunchBrokerToBank;
%ignore THOST_FTDC_FTC_BrokerLaunchBrokerToBank;
%feature("director") CThostFtdcTraderSpi;
%include "ThostFtdcUserApiDataType.h"
%include "ThostFtdcUserApiStruct.h"
%include "ThostFtdcTraderApi.h"
以上文件的%typemap部分的作用大概是,对每一个从 Java 获取 C++ 里的 String 都做编码的转换,从 C++ 的 GB2312 转为 Java 的 UTF-8。这样 Java 服务可以正确获取 CTP 返回的中文字符串,例如订单被拒绝的原因。
二、创建文件夹
在工作目录创建 src 文件夹,用来放生成的.java文件;以及 ctp 文件夹,然后在 ctp 文件夹内创建 thosttraderapi 子文件夹,用来打包 jar。
mkdir src mkdir -p ctp/thosttraderapi
三、使用 SWIG
在工作目录执行命令
swig -c++ -java -package ctp.thosttraderapi -outdir src -o thosttraderapi_wrap.cpp thosttraderapi.i
可能要执行 1 分钟左右,并且可能有一个 Warning 514:xxxxxxxxx 警告,忽略之。
在工作目录可以看到生成了 thosttraderapi_wrap.cpp 和 thosttraderapi_wrap.h 两个文件。此外,在 src 文件夹中还生成了几百个 .java 文件。
四、编译、打包 Java 文件
在终端中切换到 src 目录内,运行以下命令进行编译:
javac *.java
然后把编译出来的 .class 文件移动到工作目录的 ctp/thosttraderapi 文件夹内:
mv *.class ../ctp/thosttraderapi/
终端回到工作目录,执行
jar cf thosttraderapi.jar ctp
就得到了 thosttraderapi.jar,这是我们 Java 编写程序时导入的包。
4. 编译 wrapper 动态库
一、准备静态库 libiconv
首先下载静态库 libiconv.a 放到工作目录。它是把 CTP 返回的 GB2312 的中文信息转换为 UTF-8 所需的依赖库,否则在 Java 程序中会变成乱码。
至于为什么是静态库,是因为要把这个库链接到最终的 .so 动态库中,而不用另外再放一个 libiconv.so 动态库。为什么是下载一个而不是自己编译,是因为本人水平有限,我尝试从 libiconv 源码编译出来了一个 libiconv.so 动态库,但是不懂怎么编译一个静态库。
此外,参考文档的大神作者还提供了不需要 libiconv 的做法,只靠 C++ 标准库的函数就可以实现转码,我照着做发现是可以的,但生成的 .so 文件会变大一些。具体步骤是把第一步的 thosttraderapi.i 的内容改为以下的,其余步骤不变:
%module(directors="1") thosttradeapi
%{
#include "ThostFtdcTraderApi.h"
#include <codecvt>
#include <locale>
#include <vector>
#include <string>
using namespace std;
#ifdef _MSC_VER
const static locale g_loc("zh-CN");
#else
const static locale g_loc("zh_CN.GB18030");
#endif
%}
%typemap(out) char[ANY], char[] {
const std::string &gb2312($1);
std::vector<wchar_t> wstr(gb2312.size());
wchar_t* wstrEnd = nullptr;
const char* gbEnd = nullptr;
mbstate_t state = {};
int res = use_facet<codecvt<wchar_t, char, mbstate_t>>
(g_loc).in(state,
gb2312.data(), gb2312.data() + gb2312.size(), gbEnd,
wstr.data(), wstr.data() + wstr.size(), wstrEnd);
if (codecvt_base::ok == res)
{
wstring_convert<codecvt_utf8<wchar_t>> cutf8;
std::string result = cutf8.to_bytes(wstring(wstr.data(), wstrEnd));
$result=JCALL1(NewStringUTF,jenv,result.c_str());
}
else
{
std::string result;
$result=JCALL1(NewStringUTF,jenv,result.c_str());
}
}
%feature("director") CThostFtdcTraderSpi;
%ignore THOST_FTDC_VTC_BankBankToFuture;
%ignore THOST_FTDC_VTC_BankFutureToBank;
%ignore THOST_FTDC_VTC_FutureBankToFuture;
%ignore THOST_FTDC_VTC_FutureFutureToBank;
%ignore THOST_FTDC_FTC_BankLaunchBankToBroker;
%ignore THOST_FTDC_FTC_BrokerLaunchBankToBroker;
%ignore THOST_FTDC_FTC_BankLaunchBrokerToBank;
%ignore THOST_FTDC_FTC_BrokerLaunchBrokerToBank;
%feature("director") CThostFtdcTraderSpi;
%include "ThostFtdcUserApiDataType.h"
%include "ThostFtdcUserApiStruct.h"
%include "ThostFtdcTraderApi.h"
二、Makefile
先把 thosttraderapi_se.so 重命名为 libthosttraderapi.so。然后新建 Makefile,内容如下:
OBJS=thosttraderapi_wrap.o
INCLUDE=-I./ -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux
TARGET=libthosttraderapi_wrap.so
CPPFLAG=-shared -fPIC
CC=g++
LDLIB=-L. -lthosttraderapi
$(TARGET) : $(OBJS)
$(CC) $(CPPFLAG) $(INCLUDE) -o $(TARGET) $(OBJS) $(LDLIB) ./libiconv.a
$(OBJS) : %.o : %.cpp
$(CC) -c -fPIC $(INCLUDE) $< -o $@
clean:
-rm -f $(OBJS)
-rm -f $(TARGET)
install:
cp $(TARGET) /usr/lib
若使用的是不需要 libiconv 的方案,则应把 ./libiconv.a 删去。
三、编译
最后,我们执行 make,即可得到 libthosttraderapi_wrap.so。
四、使用
把 libthosttraderapi.so, libthosttraderapi_wrap.so 放到 java.library.path 下,在 Java 代码中导入 thosttraderapi.jar,就可以开始开发了。
Windows
参考大神的方法,把 Windows 下的支持也做了,再次感谢大神的分享。
1. 使用 SWIG 得到 Java 包
获取 .jar 包的步骤是和 Linux 版完全一致的,如果已经做好了 .jar 包,可以直接拿来用,不需要重复制作。
2. 编译 wrapper 动态库
也需要准备一个thosttraderapi.i(上文有),我这里用的是本文中提到的不需要 libiconv 的版本,这样可以减少一个和操作系统有关的变量。执行同样的命令
swig -c++ -java -package ctp.thosttraderapi -outdir src -o thosttraderapi_wrap.cpp thosttraderapi.i
得到thosttraderapi_wrap.h, thosttraderapi_wrap.cpp。
接下来打开 IDE,我这里用的是 Visual Studio 2019 Community。
创建一个 C++ 的 DLL 项目,工程名为thosttraderapi_wrap:


创建好项目,可以看到自带了一些文件,例如pch.h,好像是不需要的(C++我实在不懂呀),删除之。然后把以下文件复制到项目中:
ThostFtdcTraderApi.h
ThostFtdcUserApiDataType.h
ThostFtdcUserApiStruct.h
thosttraderapi.lib
thosttraderapi_wrap.cpp
thosttraderapi_wrap.h
然后将这些文件添加到工程的“源文件”中。
接下来把配置切换为Release,平台切换为x64(应该是要和 Java 的匹配,我的是64位 Java,之前编译的是32位的,运行会报错)。
还有一些项要配置的。打开项目的属性,定位到“配置属性” – “VC++目录”,“包含目录”加上 JDK 的include和win32\include目录。参考下图

继续设置参数。定位到“配置属性” – “C/C++” – “代码生成”,运行库选择“多线程(/MT)”:

最后,来到“配置属性” – “C/C++” – “预编译头”,选择“不使用预编译头”。

就可以开始编译。如果没有报错,编译出来thosttraderapi_wrap.dll就成功了。其他开发和 Linux 是一样的就不多说了。
相关参考
- CTP JAVA API(JCTP)编译(利用Swig封装C++动态库)windows版
- CTP JAVA_API(JCTP)编译(利用Swig封装C++动态库)linux版64位
- Swig转换C++接口中文乱码解决方案
- JAVA封装CTP API中文乱码解决方案
附录1
根据实际使用的经验,发现少数券商返回的某些字符中会含有乱码,造成转为 UTF-8 字符串时出错,导致字符串数据丢失。针对该问题,在使用 libiconv 的情况下,我把 thosttraderapi.i 的 if ($1) 中的代码改为
iconv_t cd = iconv_open("utf-8//IGNORE", "gb2312");
if (cd != reinterpret_cast<iconv_t>(-1)) {
char buf[4096] = {};
char **in = &$1;
char *out = buf;
size_t inlen = strlen($1), outlen = 4096;
iconv(cd, in, &inlen, &out, &outlen);
$result = JCALL1(NewStringUTF, jenv, (const char *)buf);
iconv_close(cd);
}
问题似乎得到了解决。
附录2
对于很长的字符串,例如日结单,CTP 分多次返回,用户需要将字符串拼接起来成完整的结单。但是按以上 SWIG 的做法,每一次 get 这个 String 都先进行转码,由于一个中文字符大于 1 个字节,然后再把转码后的字符串进行拼接,拼接的地方可能刚好位于一个中文字符的中间,这个位置就可能出现乱码的情况。(相关参考第 4 条)
解决的办法,需要改动生成的 wrapper 的 C++ 代码。首先找到Java_ctp_thosttraderapi_thosttradeapiJNI_CThostFtdcSettlementInfoField_1Content_1get这个函数。
然后大概有两个方案。
第一个方案保留这个函数的返回类型,即保持返回jstring类型,但是在传给jenv->NewStringUTF函数时,把传入的 char* 改成不会有编码问题的编码,例如 base64 编码,或者转成纯数字。这样改动量比较小,但是改变了编码,需要额外的内存空间,回到 Java 还要再转回 byte[],效率较低。
另一个方案是把这个函数的返回类型改为jbyteArray,参考本文参考的第 4 条。这个方案需要改动 C++、Java、Java 库中的 CThostFtdcSettlementInfoField.java 等文件,技术难度稍高,但是效率更高。
发表评论