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之间的数据的传递过程如下:

                                                                              

java封装 cs程序 档案 java封装c接口_java封装 cs程序 档案

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实现对话的流程如下:

                                                             

java封装 cs程序 档案 java封装c接口_Java_02

系统主要分四部分:系统初始化、用户输入读入、对话处理、退出系统。

无论是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中。