CTP Java API Linux/Windows x64 编译(SWIG 封装 C++ 动态库),并解决中文乱码问题

前言

网上有不少教程讲到 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.cppthosttraderapi_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

选择 DLL
填写项目名称

创建好项目,可以看到自带了一些文件,例如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 的includewin32\include目录。参考下图

选择了 Release x64;添加了包含目录;“源文件”添加了 6 个文件(图片可点击放大)

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

运行库

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

不使用预编译头

就可以开始编译。如果没有报错,编译出来thosttraderapi_wrap.dll就成功了。其他开发和 Linux 是一样的就不多说了。

相关参考

  1. CTP JAVA API(JCTP)编译(利用Swig封装C++动态库)windows版
  2. CTP JAVA_API(JCTP)编译(利用Swig封装C++动态库)linux版64位
  3. Swig转换C++接口中文乱码解决方案
  4. JAVA封装CTP API中文乱码解决方案

附录1

根据实际使用的经验,发现少数券商返回的某些字符中会含有乱码,造成转为 UTF-8 字符串时出错,导致字符串数据丢失。针对该问题,在使用 libiconv 的情况下,我把 thosttraderapi.iif ($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 等文件,技术难度稍高,但是效率更高。


发表评论