前言
网上有不少教程讲到 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 等文件,技术难度稍高,但是效率更高。
发表评论