将 Java VM 嵌入 C 应用程序并使用带 JSON 的 JNI






4.89/5 (7投票s)
探索如何在 C 应用程序中嵌入 Java VM,并使用 JNI 为 Java 应用程序开发本机库。
引言
不久前,我将一个简单的 Web 服务器功能集成到一个用 C 和 C++ 编写的应用程序中。这个服务器主要处理 HTTP GET 请求,并借助一些客户端 JavaScript 和几个内置的 URI 提供专门的数据检索服务,我得以探索这项技术的一些可能用途。
然而,我还有其他事情要做,而不是构建和维护一个嵌入式 Web 服务器,所以我开始寻找替代方案,并找到了 Jetty (http://eclipse.org/jetty/)。Jetty 看起来是一个很棒的解决方案,提供了大量功能和服务,稳定可靠,最重要的是,将由其他人来维护它。Jetty 也可以嵌入到我的应用程序中。然而,Jetty 是一个 Java 应用程序,所以我需要了解如何在 C 应用程序中嵌入 Java 虚拟机,并为 Java 应用程序提供一个本机库,以便将 C 应用程序数据暴露给 Java 应用程序。
我还借此机会使用了 JSON 和两个可用的 JSON 包。第一个是 Dave Gamble 的 cJSON 包 (http://sourceforge.net/projects/cjson/),用于 C 应用程序;另一个是 JSON simple 包 (https://code.google.com/p/json-simple/),用于 Java 应用程序。JSON 提供了在二进制不兼容的组件之间进行通信的能力,类似于 XML 提供的功能。然而,对于简单的需求,JSON 似乎更容易使用。
背景
通过本文及其源代码,我尝试记录在探索将 Java 虚拟机 (VM) 嵌入 C/C++ 应用程序以及使用 JNI 提供本机库以提供额外功能的过程中学到的经验。作为此次探索的一部分,我还研究了使用 JSON 在 Java 端和 C 端之间通信复杂数据结构。
您需要具备一些 C 语言经验,并且熟悉指针,包括函数指针,才能理解此次探索的代码。您还需要具备一些 Java 经验。
我使用 Visual Studio 2005 完成这项工作,但是由于这只涉及几个文件和 Visual Studio 解决方案中的两个项目,所以这项工作应该很容易移植到更新版本的 Visual Studio。
我很高兴地发现,在 Visual Studio 2005 中以逐步细化的方式处理 Java 源文件非常简单。我添加了一个自定义构建步骤,以便在构建时调用 javac 编译器。我在 Java 源文件 helloworld.java 中添加了注释,记录了自定义构建步骤。从工作角度来看,我遇到的主要问题是识别生成 javac 编译器错误的具体行。
由于我使用的是像 strcpy()
和 sprintf()
这样的旧 C 标准库函数,Visual Studio 2005 编译器会发出警告,建议改用这些函数的安全版本。在这个示例中,我忽略了这个问题。
Using the Code
Visual Studio 2005 解决方案分为两个项目。项目 jvm_test 包含 C 应用程序 jvm_test.cpp 和 Java 应用程序 helloworld.java 源文件,并构建主应用程序。项目 jvm_data_if
包含 Java 应用程序使用的本机函数的 C 源文件,并构建 Java 应用程序使用的动态链接库 jvm_data_if.dll。
cJSON 库的源代码已下载并放入名为 cJSONFiles 的文件夹中,作为 jvm_data_if 项目文件夹的子文件夹。我删除了不必要的文件以减少混乱,并未对源代码进行任何更改。
顺便提一下,我曾想过 cJSON 库是否能提供一种方法,让变量展现出一种变色龙般的特性,即根据变量的使用方式暴露出不同的类型。在某些情况下,能够以 int
或 char string
的形式访问变量内容,似乎可以提供一种灵活性,这种灵活性值得在内存和 CPU 方面付出额外的开销。例如,在 helloworld.java 应用程序中,我使用自动转换功能从各种部分构建一个 string
。
执行环境还要求 JSON simple jar 文件可供 Java 应用程序使用。有一个名为 class_jars 的文件夹,它与构建输出文件夹和 .sln Visual Studio 解决方案文件位于同一级别,JSON simple jar 文件就放在那里。在这个例子中,Java VM 的启动包括使用相对路径指定 -Djava.class.path
,该路径指定包含 JSON simple 类 json-simple-1.1.1.jar 的 jar 文件的 class_jars 文件夹。
由于构建输出的一部分是 javac 编译器编译 helloworld.java
源文件后创建的 helloworld.class
文件,因此我们必须包含 helloworld.class
类文件所在目录的路径。在此例中,我们也使用相对路径,但该路径会因调试构建还是发布构建而异,因此我们使用 C 预处理器与 #if
来确定需要使用哪个版本的 -Djava.class.path
。
最后,我使用 Win32 API 为 Java 虚拟机启动一个线程。在这个例子中,主线程没有做任何其他事情,它只是等待运行 Java VM 的线程完成并退出。然而,未来还有一些想法。
未来展望
本次探索的下一阶段将是制作一个更复杂的示例,其中 Java 应用程序启动一个 Jetty 实例。一个需要探索的领域是如何让 Java 应用程序与主 C 应用程序进行通信。目前,本机库是一个独立于主 C 应用程序的 DLL。
然而,目标是拥有一个提供 Web 服务器功能并充当 C 应用程序与任何希望与 C 应用程序交互的 Web 客户端之间中介的 Java 应用程序。一些可能性包括使用共享内存、文件系统或进程间通信,例如带有 TCP 或 UDP 的管道或套接字。
另一种可能性是使用 DLL 作为中介,因为 C 应用程序、Java 应用程序和动态链接库都位于同一地址空间中。由于所有组件都位于同一地址空间中,C 应用程序可以通过向 DLL 提供一个包含函数和数据指针的 struct
,从而向 Java 应用程序提供访问 C 应用程序中函数和数据的权限,该 struct
将由 DLL 的函数用于为 Java 应用程序提供对这些函数和数据的访问。
关于源代码更新的评论 2015 年 1 月 20 日
出于好奇,想知道如何在原生 C 端通过 Java VM 函数访问 Java 对象的字段,我花了一些时间编写了一个原生函数 dumpMe()
,它将 helloworld
类的各种字段打印到标准输出设备或控制台窗口。dumpMe()
函数打印出 myInt
和 myString
字段(分别为一个 int
和一个 String
),然后打印出 myStruct
对象的字段,该对象是一个 RecordStruct
类。这似乎捕捉到了更复杂字段处理的基础。
为了使事情变得更容易,我在原生 C 端创建了一些基础设施,两个 struct
用于表示 helloworld
和 RecordStruct
这两个类,以及两个函数:fill_helloworld()
用于填充一个 struct Java_helloworld
,它将接受一个 Java 对象并在原生 C 端创建一个有用的表示。请注意,fill_helloworld()
函数调用了第二个函数 fill_RecordStruct()
来处理 helloworld
类的 myStruct
字段的转换。
用于在原生端表示 Java 类的这两个原生 C struct
的源代码如下所示
// represent the RecordStruct class
typedef struct {
jstring key;
jstring firstName;
jstring lastName;
jint age;
} Java_RecordStruct;
// represent the helloworld class.
typedef struct {
jint myInt;
jstring myString;
Java_RecordStruct myStruct;
} Java_helloworld;
通常,过程是使用 Java VM 函数 GetObjectClass()
获取一个类描述句柄,类型为 jclass
,然后使用 GetFieldID()
函数获取类中字段的字段标识符。一旦有了字段标识符,就可以从对象中获取字段的值。
以下代码片段展示了基本过程。请注意,Java int
内置类型在原生 C 端有一个直接的对应物 jint
,而 String
在原生 C 端被视为一个对象。结果是,为了访问 String
中的实际文本,需要另一个步骤,请求 Java VM 提供一个包含文本的字符数组。然而,将其保留为 jstring
并在必要时才进行转换似乎是最佳做法。
jclass helloworld_obj = (*env)->GetObjectClass(env, obj);
jfieldID fid_myInt = (*env)->GetFieldID (env, helloworld_obj, "myInt", "I");
jfieldID fid_myString = (*env)->GetFieldID (env, helloworld_obj, "myString", "Ljava/lang/String;");
myWorld->myInt = (*env)->GetIntField (env, obj, fid_myInt);
myWorld->myString = (*env)->GetObjectField (env, obj, fid_myString);
顺便说一下,在上面的例子中,指针 myWorld
指向原生 C struct
Java_helloworld
,我们用它来保存 Java 对象在原生端的原生表示。使用 struct
来表示 Java 对象似乎相当直观,并且使用辅助函数执行转换很好地封装了执行转换的源代码。
还要注意的是,有些字段具有短字段类型,例如 int
字段的 "I"
,而另一些则具有我称之为完全限定类型,例如 String
使用 "Ljava/lang/String;"
,或者 RecordStruct
类型的 myStruct
对象使用 "Lhelloworld$RecordStruct;"
。最后一个使用美元符号将 helloworld
类封装的 private
类 RecordStruct
与其封闭类分开。
要记住的一点是,此 struct
的某些成员并非实际实体,而是 Java VM 中相应 Java 对象实体的一种句柄。因此,要真正获取对象的值,您需要使用 Java VM 函数进行必要的转换。一个例子是 Java 类中的 String
字段。实际访问 string
文本的过程需要使用 jstring
句柄来请求 Java VM 提供一个包含 String
文本的 char
数组。因此,要访问 String
的实际文本,您需要一系列如下所示的源代码行
const char *strKey;
strKey = (*env)->GetStringUTFChars(env, myWorld.myString, NULL);
printf (" helloworld.myString %s\n", strKey);
(*env)->ReleaseStringUTFChars(env, myWorld.myString, strKey);
请注意,在此示例中,char
指针 strKey
是一个 const char *
。Java VM 通过 GetStringUTFChars()
函数提供的指针和所指向的文本不应以任何方式修改。
最后,我尝试更新了注释,以提供更多信息并更好地组织源代码。
还有一个关于 GetFieldID()
函数的问题。网络上有很多文章指出 GetFieldID()
函数可能会影响性能。因此,这个函数应该尽可能少用,包括缓存字段标识符。字段标识符是在类加载时创建的,并且在类卸载之前保持相同的值。我遇到的一种建议是,在类加载时调用一个本地 C 函数,允许本地功能获取所需的字段标识符并缓存它们。这种方法可以提高具有许多类的大型 Java 应用程序的性能。然而,在这个示例中,任何这样的改进都不会被注意到。最起码的预防措施是将所有使用 GetFieldID()
的地方从循环中提取出来。
关于源代码更新的评论 2015 年 1 月 25 日
这是目前设想的最后一次更新。本次更新进一步探讨了使用 Java 虚拟机的 C 应用程序。在此版本中,C 应用程序将函数和数据导出到由 C 应用程序启动的 Java VM 中运行的 Java 应用程序。
源代码更改简要概述
两个项目都新增了几个文件,以减少对本文源代码早期版本现有文件的更改。然而,新功能确实需要修改 helloworld.java 中的 Java 应用程序,以便使用添加到 jvm_data_if.dll 的新功能,以及对 jvm_test.cpp 文件中的 C 应用程序源代码进行一些更改。
helloworld.java
生成的输出已进行了一些改进,并且作为新功能测试的一部分,还增加了额外的输出。
我还对项目的属性进行了一些调整,最重要的是对 jvm_test
项目进行了调整,以便 C 应用程序可以与 jvm_data_if.dll
及其库链接。我还向 jvm_test
项目添加了一个新的 .bat
文件,其中包含针对调试构建生成的 Java 类文件运行 javah
和 javap
实用程序所需的命令。这些实用程序生成的信息对 JNI 程序员很有用。
更改详情
我们使用由 jvm_data_if
项目创建的动态链接库 jvm_data_if.dll
,作为 C 应用程序向 Java 应用程序提供对导出数据和导出函数访问的一种方式。基本过程是 C 应用程序调用 jvm_data_if.dll
中的初始化函数,然后启动带有 Java 应用程序的 Java VM。
在这个例子中,C 应用程序有三种不同的导出接口版本。Java 应用程序能够决定要使用哪种接口版本,请求该接口版本,然后使用它。这种接口版本理念被各种不同的商用现成 (COTS) 组件(如 Microsoft Direct-X)使用。其思想是,用户应用程序可以编写到特定的接口,而接口供应商可以使用新的接口版本提供包含新功能的更新,而不会影响继续使用旧接口的旧用户应用程序。
项目 jvm_test
中创建 C 应用程序的源文件 exportedfuncs.cpp
包含 C 应用程序导出的函数和数据。在 C 应用程序启动 Java VM 之前,它会调用函数 Java_exportedfuncsfactory()
,提供函数 ExportedFuncsFactory()
的指针。函数 ExportedFuncsFactory()
在源文件 exportedfuncs.cpp
中定义,当调用时指定一个特定的版本号,它将返回一个指向所请求接口版本的指针。接口版本是指向由函数指针组成的 struct 的指针,这些函数指针指向该接口版本的函数。
顺便提一下,JNI 库通过 JNIEnv 指针似乎也使用了类似的方法。这个指针指向一个结构体,该结构体包含 Java 虚拟机导出的各种 JNI 函数,这些函数通过该类型的指针访问,例如 (*env)->GetMethodID(...)
。
因此,C 应用程序创建了一个接口工厂函数,其形式如下。此函数在头文件 exportedfuncs.h
中定义,该文件也包含各种接口版本的声明。此头文件也被 jvm_data_if
项目中的本地方法文件 exportedfuncs.c
包含,该文件提供了 Java 应用程序使用的 JNI 函数。jvm_test
项目中的 exportedfuncs.h
提供了 C 应用程序导出的函数和数据的接口规范,因此它必须包含在 Java 应用程序的 JNI 中,以便 C 应用程序和 Java 应用程序都了解 C 应用程序导出的接口和数据。
ExportedFuncsUnion ExportedFuncsFactory (ExportedFuncsTypes iType)
{
ExportedFuncsUnion retValue = {ExportedFuncsType_0, 0};
retValue.iType = iType;
switch (iType) {
case ExportedFuncsType_1:
retValue.u.pFuncs_1 = &FuncsStruct_1;
break;
case ExportedFuncsType_2:
retValue.u.pFuncs_2 = &FuncsStruct_2;
break;
case ExportedFuncsType_3:
retValue.u.pFuncs_3 = &FuncsStruct_3;
break;
}
return retValue;
}
在为 Java 应用程序初始化环境时,C 应用程序调用 jvm_data_if.dll
中的一个函数,该函数旨在向 Java 应用程序使用的本地方法提供工厂函数,如下所示。
// the function prototype is declared in data_get_setter.h in jvm_data_if project.
// this function is part of the jvm_data_if.dll and is used to provide to the
// Java application we are starting up the functions that the C application is exporting.
Java_exportedfuncsfactory (ExportedFuncsFactory, ExportedFuncsType_1);
因此,提供不同接口版本涉及相当多的间接性。首先,C 应用程序必须向 jvm_data_if.dll
提供一个函数指针,该函数将提供不同版本的接口。这些不同版本的接口,类似于 C++ vtable 或 COM 实例,是一个函数指针结构体。jvm_data_if.dll
提供给 Java 应用程序的本地函数通过其中一个可能的结构体访问 C 应用程序函数。
所有这些都汇集在位于 jvm_data_if
项目的源文件 exportedfuncs.c
中的 JNI 函数 Java_helloworld_00024ExportedFuncs_getExportedData()
中。该函数在下面复制以展示 Java VM 函数的使用,这些函数需要从类创建实例,获取 C 应用程序接口的正确版本,然后使用该接口获取数据以填充新创建的对象。
/*
* getExportedData method of the ExportedFuncs inner class of the helloworld class to
* create an object of the ExportedData class and provide the data back to the c
* as an object of the class.
* public native ExportedData getExportedData(int iVal);
*/
JNIEXPORT jobject JNICALL Java_helloworld_00024ExportedFuncs_getExportedData (JNIEnv *env, jobject obj, jint iValue)
{
jfieldID fid = (*env)->GetFieldID(env, (*env)->GetObjectClass(env, obj), "theExportedData", "Lhelloworld$ExportedFuncs$ExportedData;");
jobject newObj = 0;
jclass cls = (*env)->FindClass(env, "Lhelloworld$ExportedFuncs$ExportedData;");
// Get the Method ID of the constructor for this inner class.
// There are two things to notice about this GetMethodID() function call.
// First, the constructor is requested by specifying the special string "<init>"
// Second, the signature of the constructor includes the enclosing class in the signature.
// Also there are no arguments for this constructor. if there were then they would need to be included between the parenthesis
// for example "(Lhelloworld$ExportedFuncs;I)V" for a single int arg.
jmethodID midInit = (*env)->GetMethodID(env, cls, "<init>", "(Lhelloworld$ExportedFuncs;)V");
if (NULL == midInit) return NULL;
// Call the class constructor to allocate a new instance. the default constructor has no arguments.
newObj = (*env)->NewObject(env, cls, midInit);
// now lets set some values in our new object and return it if the object exists.
if (newObj) {
// we were able to create the object so lets fill in the fields of the ExportedData
// by fetching the data from the C application using the interface provided.
const char *theString = 0;
jfieldID fid_theString = (*env)->GetFieldID (env, cls, "theString", "Ljava/lang/String;");
jfieldID fid_theInt = (*env)->GetFieldID (env, cls, "theInt", "I");
jstring versionString = 0;
if ((theString = Java_helloword_ExportedFuncsInterface.getStr (iValue)) != 0) {
// if the requested string is in the C application table then generate
// a Java String. otherwise the value will be NULL.
versionString = (*env)->NewStringUTF (env, theString);
}
(*env)->SetIntField (env, newObj, fid_theInt, iValue);
(*env)->SetObjectField (env, newObj, fid_theString, versionString);
}
return newObj;
}
在 Java 应用程序源文件 helloworld.java
中,在构造 ExportedFuncs
对象时,作为创建 helloworld
实例的一部分,会调用此本地方法 getExportedData()
,目的是使用默认数据值创建一个 ExportedData
对象。作为探索 JNI 的一部分,我还使用了一个超出范围的值进行调用,以查看我的本地方法如何处理超出范围的情况。
javah 和 javap 工具
随着本文版本的更改,我向 jvm_test
项目添加了一个 .bat
文件,genjavah.bat
。这个 .bat
文件使用 javah
和 javap
标准 Java 工具来提供关于 helloworld.class
文件的信息。我没有意识到这些实用程序在与 JNI 合作时,通过正确的命令行选项可以生成多么有用的信息。这个 .bat
文件确实使用了调试构建生成的 Debug 文件夹中的 .class
文件,因此在运行 .bat
文件之前需要进行一次调试构建,以便使用 javah
实用程序生成头文件存根,并生成一个包含 javap
实用程序输出的文件 helloworld_javap.txt
。
javah
和 javap
实用程序的输出都对使用 JNI 的程序员有所帮助。例如,javap
实用程序可以帮助提供变量的完全限定名称和方法签名。javac
Java 编译器在 .class
文件中生成本地方法签名,javah
实用程序可以读取这些签名以生成相应的 C 语言函数原型。javap
实用程序生成的输出对于具有内部类等的复杂类结构尤其有用。
最后几点思考
这一系列文章和源代码更新中,探索 JNI 潜力的工作一直很有趣。由于源代码被修改、更改和更新,它有些混乱,也暴露出一些错误的开始。对此我深表歉意。
我尝试调查了一些 JNI 程序员可能面临的问题领域,这些问题基于本次探索中产生的问题以及我在互联网上各种论坛中看到的问题。我将散落在各处的零碎知识点汇集起来,试图从零散的碎片中创造出一幅完整的拼布。
我怀疑,通过迭代不同版本来探索各种主题的方法,可能导致我错失了一种更抽象的方法。我所做的大部分工作都是过程性的,使用 Java VM 的 JNI 函数以与特定类结构相关的逐步过程进行操作。我的印象是,Java VM 维护着相当多的信息,JNI 程序员应该能够查询环境和对象,以提供一种更抽象的方法。
关注点
Java VM 中的 CLASSPATH 环境变量
在嵌入 Java 虚拟机时,我遇到的一个关键点是 Java VM 似乎没有识别或使用 CLASSPATH
环境变量。当我尝试在 Java 应用程序中使用 JSON simple 组件,并在 CLASSPATH
中添加 jar 文件的路径后,Java 应用程序失败了。我在 Visual Studio 调试器中看到的是 Java VM 线程几乎立即发出完成信号,控制台窗口中没有任何输出。
经过一番探究,通过在 Java 应用程序中注释掉源代码并移动 System.out.println()
调用以获取一些输出,我发现问题在于 JSON simple 解析器对象的创建。我最终修改了 Java VM 启动参数,在 -Djava.class.path
参数中包含了 JSON simple jar 文件的路径,Java 应用程序不再崩溃。这让我得出结论:当在 C 应用程序中启动 Java VM 时,C 应用程序必须在启动时使用 -Djava.class.path
作为虚拟机参数来复制 CLASSPATH
环境变量。
通过 JNI 调用的本地函数的符号和接口
虽然通常的程序是使用 javah
工具生成 C 函数的样板或存根,但我却按照看起来是标准的方式手工编写了 C 函数。对于这个简单的例子,这种方法运行良好。我的理解是本地函数名会包含包路径作为命名约定的一部分,然而在这个例子中我没有使用包。
接口错误导致 Java VM 意外终止
有几次,我修改了 Java 应用程序中某个本地方法的接口,却忘记在本地端的实际 C 函数原型中进行类似的修改。这导致 Java 应用程序中出现运行时错误,应用程序会意外终止。在我的 Visual Studio 2005 开发环境中,关于终止的信息是空白的。在 C 函数中设置断点表明 Java VM 在调用 C 函数之前就终止了。在某些情况下,调试器会显示实际的运行时错误,但在大多数情况下,Java VM 只是终止了,调试器的输出窗口中没有错误指示,也没有任何错误指示。
所有由 Java 应用程序调用的原生 C 函数都具有相同的头两个参数
JNIEnv *env
,这是一个指向包含各种 Java VM 接口函数函数指针的struct
的指针;jobject obj
,这是一个不透明指针,指向在 Java 应用程序中定义本地函数的对象。
struct JNIEnv
类似于 C++ vtable
(虚表)或 COM 接口指针。它提供了一种方法来访问从 Java VM 到原生 C 表示形式的函数和数据转换。jobject
是一个不透明指针,指向一个 Java 对象,其字段通过 JNIEnv
指针公开的各种 Java VM 函数进行访问。
在 C 源代码中使用 JNIEnv 指针
在使用 C 语言时,关于通过 JNIEnv
指针公开的函数,需要记住一个重要点。JNI 接口是 (JNIEnv *env, jobject obj, ...)
,所以要使用像 GetObjectClass()
这样的 JNI 函数,C 语言使用的语法是 (*env)->GetObjectClass(env, obj);
,它将解引用 JNIEnv
指针并通过存储在 JNIEnv
结构体中的函数指针调用 GetObjectClass()
函数。第一个参数始终是 C 源代码的 JNIEnv
指针变量。
JNI 头文件提供了 C++ 源文件将 JNI 函数封装在一个 struct
中的功能,该 struct
将 JNIEnv
指针作为 this 指针提供。但是,C 源文件要求 JNIEnv
指针作为第一个参数,否则您将遇到编译错误。我在 Visual Studio 2005 中遇到的问题是,智能感知提示提供了 C++ 的函数原型,所以我必须记住忽略提示,并始终以 JNIEnv
指针作为 JNI 函数的第一个参数。
访问 Java 对象变量和 JSON 的原理
在本地函数中访问 Java 对象变量有点繁琐,需要使用一系列函数调用来查找对象中的变量,然后使用 getter 或 setter 类型的函数访问它。例如,以下是一个访问 int
变量、获取当前值、将变量设置为零然后返回原始值的小函数。
JNIEXPORT jint JNICALL Java_helloworld_dataClearMyInt (JNIEnv *env, jobject obj)
{
// get the reference to the actual object then we will search for the variable
// we want to modify and if found, we will then copy the old value to return it
// and set it with a new value of zero.
jint oldInt = 0; // define the variable that will contain the old value
jclass helloworld_obj = (*env)->GetObjectClass(env, obj);
jfieldID fid = (*env)->GetFieldID (env, helloworld_obj, "myInt", "I");
if (fid) {
// this check was originally done with the expectation that we could probe the
// object to see if a variable was defined or not however what we ran into was
// the Java VM seems to detect that a GetFieldID() was done on a non-existent
// variable and it will terminate when this function returns. So sad!!!
oldInt = (*env)->GetIntField (env, obj, fid);
(*env)->SetIntField (env, obj, fid, 0);
}
return oldInt;
}
正如你所看到的,这个过程相当繁琐且容易出错。我遇到的一个问题是,如果对象中不存在该变量,那么即使我检查了结果,当本地函数返回时,Java 虚拟机仍然会退出。就好像 Java VM 不允许探测对象并对对象以及实际可用的变量做出运行时决策一样。
这就是为什么我认为使用 JSON 作为表示来在 Java 和本地之间进行通信对于更复杂的数据结构来说会非常有效。
使用 JSON 在 Java 应用程序和 C 本地之间进行通信
在我工作中遇到的一个问题是跨接口的数据封送和通信。JSON 和 XML 通过使用文本表示很好地解决了这个问题。我曾见过 XML 用于与 COM 控件通信,其中创建了一个 XML 文本字符串,并调用 COM 控件中的一个方法,将 XML 文本字符串作为参数,该方法返回另一个包含操作结果的 XML 文本字符串。
以这种方式使用 XML 形成了一个灵活的接口,其中 XML 允许指定或省略选项。它还允许一个简单的 COM 接口,使得为特定目标语言或环境开发任何所需的包装器变得简单直接。只要该语言提供了一种实例化 COM 对象和以文本 string
调用方法的方法,就可以使用 COM 控件。
虽然 XML 在此类通信以及客户端和服务器之间的 Web 应用程序中表现良好,但在过去的几年中,JSON 因其简洁性和易于与 JavaScript 结合使用而获得了更大的市场份额。有许多不同的包为 Java 和 C 以及其他语言提供 JSON 功能,但在此示例中,我为 C 本地库选择了 cJSON 库,为 Java 应用程序选择了 JSON simple 库。我选择这两个库而不是其他产品没有特别的原因。
我喜欢使用 JSON 的一点是,它可以描述一个对象或记录,并且使用它我只指定需要修改的字段来更新现有记录。在 Java 应用程序中,有一个地方我从本地库中获取一个现有记录,并只更新该记录中的 Age
数据。
// retrieve a specific record from the database which we get
// as JSON text and then store the data into our Java object.
sJson = jjj.retrieveJsonRecord("1012221234");
jjj.myStruct.storeJson(sJson);
System.out.println("sJson1 = " + sJson);
System.out.println(" sJson2 = Key \"" +jjj.myStruct.key + "\" -> "+
jjj.myStruct.lastName + ", " + jjj.myStruct.firstName + " - Age: " + jjj.myStruct.age);
// now update just the age for this dataBase record then fetch it again to check.
structString = "{\"Age\" : 45 }";
iStatus = jjj.insertupdateJsonRecord(jjj.myStruct.key, structString);
sJson = jjj.retrieveJsonRecord("1012221234");
jjj.myStruct.storeJson(sJson);
System.out.println(" sJson2 = Key \"" +jjj.myStruct.key + "\" -> "+
jjj.myStruct.lastName + ", " + jjj.myStruct.firstName + " - Age: " + jjj.myStruct.age);
在本地库端,使用 cJSON 库的 C 函数会为每个 JSON 文本关键字执行一系列 GetObjectItem()
调用。如果关键字在 JSON 文本中指定,则返回值将是一个有效的非空指针,然后我们可以访问该关键字的值。通过测试每个关键字的返回值,我们能够对记录进行特定的更改,从而允许 Java 应用程序对记录进行有针对性的修改。
// next we will process the JSON object one field at a time checking to
// see if the field was included in the JSON object. if a field is
// not specified in the JSON then we will not modify that field in the
// record we have which may be either a new record or a copy of an
// existing record we are modifying.
obj = cJSON_GetObjectItem(root,"FirstName");
if (obj)
strcpy (newRecord.m_FirstName, obj->valuestring);
obj = cJSON_GetObjectItem(root,"LastName");
if (obj)
strcpy (newRecord.m_LastName, obj->valuestring);
obj = cJSON_GetObjectItem(root,"Age");
if (obj)
newRecord.m_Age = obj->valueint;
历史
- 2015 年 1 月 18 日:初始版本
- 2015 年 1 月 20 日:更新源代码,包含使用 Java VM 函数访问字段的示例