ChatScript是一个很完整的对话框架,但是,对话系统往往并不是独立存在的,在我的应用场景下,它只是语音对话的一部分,被调用,生成完美的回复。我需要的是一个完整的语音对话APP,CS底层是C++实现的,而APP由java实现,因此,要将CS封装成一个java接口,供APP调用。
封装接口主要工作分三部分:(1). 看底层主调程序,锁定哪些函数是要被暴露出去的,需要做怎样的修改。(纯C部分)(2).定义java类中需要native化的函数,以及调用CS函数的方法。(JAVA部分)(3). 在C层面写native函数的实现。(JNI和C结合部分)
1. JNI
JNI是支持java和C之间相互调用的工具。这里更关注java、JNI、C/C++之间的数据类型转换、数据编码格式等,JNI的基本概念,请google。
JNI所定义的数据类型和C的数据类型不同,数据类型间的映射关系请参考
Java、JNI和C之间的数据的传递过程如下:
C/C++内部默认的字符串编码格式是ASCII,Java的String对象的编码格式默认为UTF-16,Java本身提供将String转为UTF-8的方法,JNI默认的编码格式是UTF-8。CS作者把系统读文本和处理文本的编码格式改为UTF-8,因此,从用户输入到获取回复文本,数据格式的转换过程为String(UTF-16) -> UTF-8 -> UTF-8 -> String。基本上只需要将Java给出的String(对应jni的jstring)转化为UTF-8的char*,将bot生成的UTF-8char*回复转化为String。用JNI提供的GetStringUTFChars()和NewStringUTF即可实现。
2. 锁定被java调用的函数
CS的主调文件是mainSystem.c,这个函数将整个底层组织成一个对话系统。mainSystem实现对话的流程如下:
系统主要分四部分:系统初始化、用户输入读入、对话处理、退出系统。
无论是local模式还是server模式,CS的输入实际上主要是stdin,即命令行输入,底层把stdin当做文件来处理。但如果作为对话处理被调用,那么,输入最好不是从文件中读,用户直接传入输入字符指针效率更高。同时,系统的循环也不需要在接口中考虑,而是由APP调用者来处理。因此,要暴露出来的函数只有InitSystem、PerfomChat、CloseSystem,这是最简单的封装。
这三个函数的原定义为:
unsignedint InitSystem(int argc,char * argv[],char* unchangedPath =NULL,char*readonlyPath =NULL,char* writablePath =NULL,USERFILESYSTEM* userfiles =NULL,DEBUGAPI in =NULL,DEBUGAPI out =NULL);
int PerformChat(char* user,char*usee,char* incoming,char* ip,char* output);
void CloseSystem();
上面提到,JNI和C/C++的数据类型不同,为了保持原程序的原始完整性,以上三个函数保持不变,使他们依然可以通过自有的MainLoop系统调用,我们新增一个cpp文件,以上每个函数用新的函数调用,而这些新的函数采用JNI的数据结构,和JNI对接。
另外,用getOutput函数读取原系统中output中的回复内容。
3. Java调用类
上面部分将C部分提供的函数确定后,新建一个java工程,定义需要native的函数(和C部分提供的向外暴露函数一致)。
public class JniUse {
// TODO Auto-generated method stub
static
{
System.load("/home/sf/workspace/CSbot/ChatScript-7.42/LIBRARY/libChatScript.so");
}
public native static String getOutput();
public native static int performChatJava(String user, String incoming);
public native static int initSystemJava(String root_path);
public native static void closeSystemJava();
public JniUse(String user){
try{
Properties prop = new Properties();
InputStream in = new FileInputStream("jniTest.properties");
prop.load(in);
String root_path = prop.getProperty("root_path");
System.out.println(root_path);
initSystemJava(root_path);
//setUserName(user);
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
}
}
生成以上类后,编译java文件:
javac JniUse.java
然后在JniUse的包目录下用class文件生成.h头文件:
javah JniTest.JniUse.h
生成的头文件为jniTest_JniUse.h。
将ChatScript整个文件夹放到工程的根目录下,为了保持CS内部的相对路径不受工程目录和机构改变的影响,文件夹一定要放在工程根目录下。
4. C对接JNI
在封装原程序前需要注意两点:
1. 在第2节提到,尽量不改变原程序,但是,由于原系统多处读和写文件,读写文件的目录都是相对路径,而将ChatScript放在java里面,程序运行时,当前目录不再是CS的目录,而是工程目录,因此,将原程序中所有读写文件路径全部添加“ChatScript/”,不然系统找不到相应的文件。
2. UTF-8的编码将一个汉字编码成3个字节,每个字节表示汉字时,第7位(最高位)为1, 原对话系统通过读文件的方式获取用户文本,在读文本时,实际将char转换成(unsigned char),然后在performChat中,通过(at<31)的方式排除特殊符号。
while (*++at)
{
if (*at < 31) *at = ' ';
if (*at == '"' && *(at-1) != '\\') quote = !quote;
if (*at == ' ' && !quote) // proper token separator
{
if ((at-startx) > (MAX_WORD_SIZE-1)) break; // trouble
startx = at + 1;
}
}
封装后舍弃读文件的环节,通过参数的方式将用户文本传入,在判断字节是否小于31前,将字节转换成(unsigned char),不然所有汉字将会被排除掉,无法得到合理输出。
接下来可以写接口文件了。
#include "common.h"
#include "jniTest_JniUse.h"
#define LOCAL_PATH_MAX 300
char* argvx1[1];
JNIEXPORT jstring JNICALL Java_jniTest_JniUse_getOutput(JNIEnv *env, jclass jc){
return env->NewStringUTF(ourMainOutputBuffer);
}
JNIEXPORT jint JNICALL Java_jniTest_JniUse_performChatJava(JNIEnv *env, jclass jc, jstring user, jstring incoming){
ReadComputerID();
PerformChat((char*)env->GetStringUTFChars(user, 0), computerID, (char*)env->GetStringUTFChars(incoming,0), NULL, ourMainOutputBuffer);
return 0;
}
JNIEXPORT jint JNICALL Java_jniTest_JniUse_initSystemJava(JNIEnv *env, jclass jc, const jstring root_path){
char* root;
root = (char*)env->GetStringUTFChars(root_path, 0);
argvx1[0] = (char*) malloc(10);
strcpy(argvx1[0], (char*)"local");
printf("%s\n", argvx1[0]);
// int i = InitSystem(1, argvx1);
if (InitSystem((int)1, argvx1)) myexit((char*)"failed to load memory\r\n");
return 0;
}
JNIEXPORT void JNICALL Java_jniTest_JniUse_closeSystemJava(JNIEnv *env, jclass jc){
CloseSystem();
}
include jniTest_JniUse.h和CS的头文件。
然后,修改SRC中的makefile,主要修改点如下
server: DEFINES+= -DLOCKUSERFILE=1 -DEVSERVER=1 -DEVSERVER_FORK=1 -DDISCARDPOSTGRES=1 -DDISCARDMONGO=1 -DDISCARDMYSQL=1
server: PGLOAD= -pthread
server: INCLUDEDIRS=-Ievserver -I../MYCODE/cppjieba-5.0.0/include/ -I../MYCODE/cppjieba-5.0.0/deps/ -I/usr/java/jdk1.8.0_101/include -I/usr/java/jdk1.8.0_101/include/linux
server: all
server: EXECUTABLE=../BINARIES/ChatScript
server: SHARE_LIB=../BINARIES/libChatScript.so
server: CFLAGS=-c -std=c++0x -Wall -funsigned-char -Wno-write-strings -Wno-char-subscripts -Wno-strict-aliasing
library: $(OBJECTS)
$(CC) $(LDFLAGS) $(DEFINES) $(INCLUDEDIRS) -fPIC -c $(SOURCES)
$(CC) -fPIC -shared $(OBJECTS) -o $(SHARE_LIB)
编译时引入jni.h所在路径
/usr/java/jdk1.8.0_101/include
和jni_md.h所在路径
/usr/java/jdk1.8.0_101/include/linux
要生成动态库,最好再生成.o文件时变使用-fPIC。
Make后得到libChatScript.so。
然后,通过JniUse.java即可实现通过java调用CS,生成自定义的对话系统了。
如果修改了脚本,需要重新加载脚本,则将可执行文件ChatScript复制一份放到java工程根目录下,在根目录下执行
./ChatScript localBuild0=ChatScript/RAWDATA/files0.txt
./ChatScript localBuild0=ChatScript/RAWDATA/filesYouFolder.txt
即可重新编译脚本,将脚本内容写入ChatScript/TOPIC中。