JAVA封装CTP API中文乱码解决方案

目前上期技术官方提供的CTP API是C++版本,但在实际使用中不少客户的量化交易系统是Java写的,这就需要有一个JAVA封装CTP API的,可供JAVA直接使用的动态库。

SWIG是一个能将C/C++接口转换为其他语言的工具,目前可以支持Python,Java,R等语言,已有成熟的教程。在用swig生成JAVA版CTP API的过程中,最大的问题莫过于返回值中文乱码问题。

本文主要介绍封装时产生的两类乱码问题,一类是普通回报中的字符串乱码问题,另一类是结算单乱码问题。其中结算单乱码问题对其他编程语言亦有参考意义。

另外,Java CTP API的完整教程看这《CTP JAVA API(JCTP)编译(利用Swig封装C++动态库)windows版》。

一、普通回报中文乱码

此类型常见的乱码主要存在于 CThostFtdcRspInfoField 结构体中的 ErrorMsg 字段,用于在接口调用存在错误时返回必要参考信息,除此之外,通过结构体 CThostFtdcInstrumentField 获取合约中文名称等信息出现乱码也是常见的乱码问题之一。

具体原因:

1. Java基于Unicode字符集,并有多个类库实现了Unicode标准,运行时内部字符串使用UTF-16,默认使用UTF-8序列化字符串。因此当JNI返回字符串时,应调用NewStringUTF方法(当输入为UTF-8时),或者调用NewString(当输入为UTF-16时)方法,最终生成可以在Java中返回的jstring。

2. CTP官方使用的是国标编码,也就是(GB18030>GBK>GB2312)中的一种。

3. SWIG封装时对JNI返回的字符串默认调用JNI中的NewStringUTF方法,显然,CTP官方使用的并不是UTF-8编码,因此出现了乱码,且这个过程中会产生信息丢失,是一个不可逆的错误。

 

存在如下两类解决方案:

第一类为定义宏 NewStringByGB2312 如下,然后搜索所有的C++文件中的 if (result) jresult = jenv->NewStringUTF((const char *)result); 用该宏替换。

define NewStringByGB2312\
if(result)\
{\
 jclass str_cls = jenv->FindClass("java/lang/String");\
 jmethodID constructor_mid = jenv-> GetMethodID(str_cls,"","([BLjava/lang/String;)V");\
 jbyteArray bytes = jenv->NewByteArray( strlen(result));\
 jenv->SetByteArrayRegion(bytes, 0, strlen(result),(const jbyte*) result);\
 jstring charsetName = jenv->NewStringUTF("gb2312");\
 jresult = (jstring)jenv->NewObject(str_cls, constructor_mid, bytes, charsetName);\
 jenv->DeleteLocalRef(str);\
 jenv->DeleteLocalRef(bytes);\
 jenv->DeleteLocalRef(str_cls);\
}\

这样操作通过C++直接调用Java中的String构造方法,传入C++中的字节数组,设置字符集,因此能够正确解析编码,转换为正确的Java字符串对象。

 

第二类为借助第三方库iconv。在SWIG配置文件中加入如下代码, 并在生成的代码中加入头文件iconv.h和相关运行库。

%include "various.i"  

%typemap(out) char[ANY], char[] {
    if ($1) {
        iconv_t cd = iconv_open("utf-8", "gbk");
        if (cd != reinterpret_cast(-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(-1))
                $result = JCALL1(NewStringUTF, jenv, (const char *)buf);
            iconv_close(cd);
        }
    }
}

这样操作生成的代码会借助iconv库在C++层面将GBK编码转换为UTF-8编码,因此JNI生成字符串对象时可以生成正确的Java字符串对象。

 

二、结算单乱码

查询结算单返回结果回调中字段Content是一个长度为501的数组。显然我们的结算单长度往往不止501,所以我们需要注意这个回调方法中还有bIsLast标志,因为结算单实际是多次回报分段传输的,且第501个字符为'\0',仅用于占位,这并不代表字符串结束。

 

在更底层的字符编码存储传输层面,我们上文提到的国标编码(GB18030>GBK>GB2312)是变长的,因此不能确保每个批次的第500位结束的时候刚好是一个字符结束,因此有可能存在一个字符所属存储编码的前n个字节存在于当前回报的数组尾部,后n个字节存在于下一次回报数组头部。如果我们不做任何修改,SWIG生成的C++和Java代码会将每次回报都直接生成一个字符串从C++返回到Java层面,因此我们会看到结算单一部分正确,一部分错误,混杂部分乱码,或者有时候完整,有时候不完整。

 

针对上述情况,我们的解决思路是对服务器返回的n个501长度的数组截取每个数组的前500位进行拼接,直到收到bIsLast标记,组成一个n*500的数组。这个操作可以在C++中完成,也可以在Java中完成,显然,在C++中完成会做较大的修改,因此我们可以选择在Java中修改。具体步骤如下。

 

在cpp中搜索CThostFtdcSettlementInfoField_1Content_1get函数,将函数返回类型改为jbyteArray,将内容改为如下:

jbyteArray jresult = 0 ;
CThostFtdcSettlementInfoField *arg1 = (CThostFtdcSettlementInfoField *) 0 ;
char *result = 0 ;

(void)jenv;
(void)jcls;
(void)jarg1_;
arg1 = *(CThostFtdcSettlementInfoField **)&jarg1;
result = (char *) ((arg1)->Content);

请注意仅需修改返回类型和内容代码,方法名称是swig生成的,不能修改。

 

完成上述步骤后,手动将 CThostFtdcSettlementInfoField.java 文件中的函数  getContent() 方法的返回类型改为byte[],将其调用的其他类的方法的返回类型也改为byte[]直到无错为止。

 

在Java中完成拼接后,使用 new String(contentBytes,"GBK"),便可得到完全正确的结算。需要提示的是,最后一组从C++返回到Java的Byte[]长度不一定是501,请根据实际长度处理。

 

特别感谢本文作者E&N无私奉献!

 

往期推荐

● CTP程序化交易入门系列之一:准备

● CTP程序化交易入门系列之二:API基本架构及初始化

● CTP程序化交易入门系列之三:获取实时行情及K线合成

● CTP程序化交易入门系列之四:行情订阅常见问题解答

● CTP程序化交易入门系列之五:现手、增仓、开平、对手盘计算

● CTP 4097错误根源

● Level-1、Level-2、快照数据、Tick数据的区别你都了解吗?

● 什么是穿透式监管,需要投资者做什么?

你可能感兴趣的:(程序化入门,C++,CTP)