龙空技术网

实现商用级Java调用C++的功能

编程实践 506

前言:

现在我们对“java调用c语言程序”大致比较关切,同学们都想要了解一些“java调用c语言程序”的相关资讯。那么小编同时在网络上搜集了一些关于“java调用c语言程序””的相关知识,希望看官们能喜欢,姐妹们快快来了解一下吧!

昨天使用某大公司提供的开发库,发现该公司底层只有一套共享库(.so文件),但是却为客户提供了C++、Java、C Sharp、Python四套接口。

根据项目需要,我们使用了Java开发接口,使用该接口时,发现Java接口代码使用JNA技术,写得非常棒,因此产生了自己也模拟写一个的想法。现在,我们实施这个想法吧。

(特此说明:由于供货商未给我们公司提供共享库的源代码,因此下面实现方案中使用的C++代码,纯属猜测和自创)

1、编写C++的程序

现在,我们编写一个读取/etc/profile文件内容,并且打印出来的功能。

1.1、建立PrintProfile工程

这里使用Eclipse工具,建立一个PrintProfile工程,工程中包含src、obj、bin三个子目录,分别用于存储源代码、目标文件、最终输出的动态库文件。

工程如下图所示:

1.2、在src目录中编写一个常量定义头文件ConstDef.h

ConstDef.h文件的内容如下:

#ifndef __CONST_DEF_H__#define __CONST_DEF_H__const int MAX_LINE_LENGTH = 1024;const int MAX_PROGRAM_NAME_LENGTH = 64;const int MAX_PROGRAM_VERSION_LENGTH = 32;const char* PROGRAM_NAME = "PrintProfile";const char* PROGRAM_VERSION = "0.8.1";#endif//__CONST_DEF_H__

现在工程的结构变为:

1.3、在src目录中编写类定义头文件PrintProfile.h

PrintProfile.h文件的内容如下:

#ifndef __PRINT_PROFILE_H__#define __PRINT_PROFILE_H__class PrintProfile{public:PrintProfile();int OpenProfile();int ReadLine(char* pcLine, int iMaxLineLength);void CloseProfile();void GetProgramName(char* pcProgramName, int iMaxProgramNameLength);void GetProgramVersion(char* pcProgramVersion, int iMaxProgramVersionLength);private:FILE* m_fpProfile;};extern "C" int OpenProfile();extern "C" int ReadLine(char* pcLine, int iMaxLineLength);extern "C" void CloseProfile();extern "C" void GetProgramName(char* pcProgramName, int iMaxProgramNameLength);extern "C" void GetProgramVersion(char* pcProgramVersion, int iMaxProgramVersionLength);#endif//__PRINT_PROFILE_H__

现在工程的结构变为:

1.4、在src目录中编写类实现文件PrintProfile.cpp

PrintProfile.cpp文件的内容如下:

#include <string.h>#include <stdio.h>#include "ConstDef.h"#include "PrintProfile.h"PrintProfile::PrintProfile(){m_fpProfile = NULL;}int PrintProfile::OpenProfile(){m_fpProfile = fopen("/etc/profile", "r");if (m_fpProfile == NULL){return -1;}return 0;}int PrintProfile::ReadLine(char* pcLine, int iMaxLineLength){if (m_fpProfile == NULL){return -1;}if (feof(m_fpProfile)){return -1;}memset(pcLine, 0, iMaxLineLength);fgets(pcLine, iMaxLineLength-1, m_fpProfile);int iLineLen = strlen(pcLine);if (iLineLen > 0 && pcLine[iLineLen-1]=='\n'){pcLine[iLineLen-1] = '\0';}return 0;}void PrintProfile::CloseProfile(){fclose(m_fpProfile);m_fpProfile = NULL;}void PrintProfile::GetProgramName(char* pcProgramName, int iMaxProgramNameLength){memset(pcProgramName, 0, iMaxProgramNameLength);strncpy(pcProgramName, PROGRAM_NAME, iMaxProgramNameLength-1);}void PrintProfile::GetProgramVersion(char* pcProgramVersion, int iMaxProgramVersionLength){memset(pcProgramVersion, 0, iMaxProgramVersionLength);strncpy(pcProgramVersion, PROGRAM_VERSION, iMaxProgramVersionLength-1);}static PrintProfile s_PrintProfile;extern "C" int OpenProfile(){return s_PrintProfile.OpenProfile();}extern "C" int ReadLine(char* pcLine, int iMaxLineLength){return s_PrintProfile.ReadLine(pcLine, iMaxLineLength);}extern "C" void CloseProfile(){s_PrintProfile.CloseProfile();}extern "C" void GetProgramName(char* pcProgramName, int iMaxProgramNameLength){s_PrintProfile.GetProgramName(pcProgramName, iMaxProgramNameLength);}extern "C" void GetProgramVersion(char* pcProgramVersion, int iMaxProgramVersionLength){s_PrintProfile.GetProgramVersion(pcProgramVersion, iMaxProgramVersionLength);}

现在工程的结构变为:

1.5、对PrintProfile.h和PrintProfile.cpp实现的解释

考虑到C++代码在编译后,将会生成C语言形式的函数,但是函数名特别长,不容易被使用,因此对于PrintProfile类的每个成员函数,都由一个全局的C语言函数来包装。

例如,这是本文写完之后,使用nm命令,查询的动态库的导出函数名:

#nm -D libPrintProfile.so00000000000014d2 T CloseProfilew __cxa_finalizeU fcloseU feofU fgetsU fopen00000000000014e9 T GetProgramName0000000000001515 T GetProgramVersionw __gmon_start__w _ITM_deregisterTMCloneTablew _ITM_registerTMCloneTableU memset0000000000001491 T OpenProfile0000000000004088 D PROGRAM_NAME0000000000004090 D PROGRAM_VERSION00000000000014a7 T ReadLineU strlenU strncpy00000000000012b4 T _ZN12PrintProfile11OpenProfileEv00000000000013bc T _ZN12PrintProfile12CloseProfileEv00000000000013ea T _ZN12PrintProfile14GetProgramNameEPci000000000000143e T _ZN12PrintProfile17GetProgramVersionEPci00000000000012f8 T _ZN12PrintProfile8ReadLineEPci000000000000129a T _ZN12PrintProfileC1Ev000000000000129a T _ZN12PrintProfileC2Ev

可以看到,对于PrintProfile类的OpenProfile成员函数,在动态库对外提供的接口中,变成了名为_ZN12PrintProfile11OpenProfileEv的函数。

1.6、在src目录中编写Makefile文件

现在,我们为编译做准备,制作Makefile文件。Makefile文件的内容如下:

CC = g++TARGETFILE = bin/libPrintProfile.soOBJFILES = obj/PrintProfile.oINCLUDEDIRS = -I srcINCLUDEFILES = src/ConstDef.h \src/PrintProfile.h.PHONY: buildbuild: $(TARGETFILE)@echo "build successfully."clean:rm -f obj/*.o$(TARGETFILE): $(OBJFILES)$(CC) $(INCLUDEDIRS) -g -shared -fPIC $< -o $@obj/PrintProfile.o: src/PrintProfile.cpp $(INCLUDEFILES)$(CC) $(INCLUDEDIRS) -c -shared -fPIC $< -o $@

现在工程的结构变为:

2、编译C++程序2.1、将文件拷贝到Linux环境

除了Eclipse生成的.project文件外,其它文件都拷贝到Linux:

2.2、执行编译

在Linux环境下,执行make命令,将生成libPrintProfile.so文件:

3、编写Java程序3.1、建立Java工程

使用IDEA开发工具,建立一个空的JavaProject工程。建立后视图如下:

3.2、在JavaProject工程下建立SpringBoot项目call-cpp

在JavaProject下建立SpringBoot项目,名称为call-cpp,建立后视图如下:

程序的pom.xml文件使用到了下面三个依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!--  --><dependency><groupId>net.java.dev.jna</groupId><artifactId>jna</artifactId><version>5.8.0</version></dependency>
3.3、新建一个接口PrintProfile

接口PrintProfile的代码如下:

package com.flying.call.cpp;import com.sun.jna.Library;public interface PrintProfile extends Library {public static final int MAX_LINE_LENGTH = 1024;public static final int MAX_PROGRAM_NAME_LENGTH = 64;public static final int MAX_PROGRAM_VERSION_LENGTH = 32;int OpenProfile();int ReadLine(byte[] pcLine, int iMaxLineLength);void CloseProfile();void GetProgramName(byte[] pcProgramName, int iMaxProgramNameLength);void GetProgramVersion(byte[] pcProgramVersion, int iMaxProgramVersionLength);}

此时视图变为:

3.4、修改CallCppApplication类

修改CallCppApplication类的代码,加入对PrintProfile的调用。修改后的代码如下:

package com.flying.call.cpp;import com.sun.jna.Native;import lombok.extern.slf4j.Slf4j;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@Slf4j@SpringBootApplicationpublic class CallCppApplication {//程序入口方法public static void main(String[] args) throws Exception{SpringApplication.run(CallCppApplication.class, args);PrintProfile printProfile = Native.load("PrintProfile", PrintProfile.class);byte[] programName = new byte[PrintProfile.MAX_PROGRAM_NAME_LENGTH];byte[] programVersion = new byte[PrintProfile.MAX_PROGRAM_VERSION_LENGTH];byte[] profileLine = new byte[PrintProfile.MAX_LINE_LENGTH];printProfile.GetProgramName(programName, PrintProfile.MAX_PROGRAM_NAME_LENGTH);printProfile.GetProgramVersion(programVersion, PrintProfile.MAX_PROGRAM_VERSION_LENGTH);String programNameString = new String(programName, "UTF-8");String programVersionString = new String(programVersion, "UTF-8");System.out.println("Program name: " + programNameString);System.out.println("Program version: " + programVersionString);if (printProfile.OpenProfile() != 0){System.err.println("Open profile failed.");return;}System.out.println("Content of /etc/profile is : ");while (printProfile.ReadLine(profileLine, PrintProfile.MAX_LINE_LENGTH) == 0){String profileLineString = new String(profileLine, "UTF-8");System.out.println(profileLineString);}printProfile.CloseProfile();}}
4、编译和运行4.1、执行编译命令

执行mvn clean package -DskipTests命令,完成编译:

编译完成后,将会得到target目录。

4.2、拷贝程序到Linux

将target目录中的cpp_call_lib目录、resources目录、cpp-call.jar文件拷贝到Linux:

4.3、将libPrintProfile.so库文件拷贝到Java程序运行目录

将libPrintProfile.so库文件拷贝到Java程序运行目录,拷贝后目录情况如下:

4.4、执行程序

首先执行下面的命令,将动态库的目录加入到LD_LIBRARY_PATH环境变量:

LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root/call-cpp

然后执行Java程序,程序的运行情况如下:

后记,编写这个例子程序,花了我3小时的时间,但是最终确实让Java调用C++类的成员函数时,就像直接调用Java方法一样,感觉非常方便。

前段时间研究JavaCV时,发现JavaCPP也提供了类似的功能,以后有空,看能不能分析一下,也写一篇文章,共享给大家。

谢谢大家!

标签: #java调用c语言程序 #java调用cpp