百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

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

bigegpt 2025-01-08 11:20 9 浏览


昨天使用某大公司提供的开发库,发现该公司底层只有一套共享库(.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.so
00000000000014d2 T CloseProfile
w __cxa_finalize
U fclose
U feof
U fgets
U fopen
00000000000014e9 T GetProgramName
0000000000001515 T GetProgramVersion
w __gmon_start__
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
U memset
0000000000001491 T OpenProfile
0000000000004088 D PROGRAM_NAME
0000000000004090 D PROGRAM_VERSION
00000000000014a7 T ReadLine
U strlen
U strncpy
00000000000012b4 T _ZN12PrintProfile11OpenProfileEv
00000000000013bc T _ZN12PrintProfile12CloseProfileEv
00000000000013ea T _ZN12PrintProfile14GetProgramNameEPci
000000000000143e T _ZN12PrintProfile17GetProgramVersionEPci
00000000000012f8 T _ZN12PrintProfile8ReadLineEPci
000000000000129a T _ZN12PrintProfileC1Ev
000000000000129a T _ZN12PrintProfileC2Ev

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

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

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

CC = g++
TARGETFILE = bin/libPrintProfile.so
OBJFILES = obj/PrintProfile.o
INCLUDEDIRS = -I src
INCLUDEFILES = src/ConstDef.h \
src/PrintProfile.h
.PHONY: build
build: $(TARGETFILE)
@echo "build successfully."
clean:
rm -f obj/*.o
$(TARGETFILE): $(OBJFILES)
$(CC) $(INCLUDEDIRS) -g -shared -fPIC lt; -o $@
obj/PrintProfile.o: src/PrintProfile.cpp $(INCLUDEFILES)
$(CC) $(INCLUDEDIRS) -c -shared -fPIC lt; -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>
<!-- https://mvnrepository.com/artifact/net.java.dev.jna/jna -->
<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
@SpringBootApplication
public 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也提供了类似的功能,以后有空,看能不能分析一下,也写一篇文章,共享给大家。

谢谢大家!

相关推荐

5分钟搭建公网https网页文件服务器,免费权威TLS证书

请关注本头条号,每天坚持更新原创干货技术文章。如需学习视频,请在微信搜索公众号“智传网优”直接开始自助视频学习前言本文主要讲解如何快速搭建一个https网页文件服务器,并免费申请权威机构颁发的tls证...

nginx负载均衡配置(nginx负载均衡配置两个程序副本)

Nginx是什么没有听过Nginx?那么一定听过它的“同行”Apache吧!Nginx同Apache一样都是一种WEB服务器。基于REST架构风格,以统一资源描述符(UniformResources...

19《Nginx 入门教程》Nginx综合实践

今天我们将基于Nginx完成两个比较有用的场景,但是用到的Nginx的配置非常简单。内部Yum源搭建内部Pip源搭建1.实验环境ceph1centos7.6内网ip:172.16....

Nginx性能调优与优化指南(nginx优化配置大全)

Nginx性能调优需要结合服务器硬件资源、业务场景和负载特征进行针对性优化。以下是一些关键优化方向和具体配置示例:一、Nginx配置优化1.进程与连接数优化nginxworker_process...

C++后端开发必须彻底搞懂Nginx,从原理到实战(高级篇)

本文为Nginx实操高级篇。通过配置Nginx配置文件,实现正向代理、反向代理、负载均衡、Nginx缓存、动静分离和高可用Nginx6种功能,并对Nginx的原理作进一步的解析。当需...

【Nginx】史上最全的Nginx配置详解

Nginx服务器配置中最频繁的部分,代理、缓存和日志定义等绝大多数功能和第三方模块的配置都在这里,http块又包括http全局块和server块。Nginx是非常重要的负载均衡中间件,被广泛应用于大型...

【Nginx】Nginx 4种常见配置实例(nginx基本配置与参数说明)

本文主要介绍nginx4种常见的配置实例。Nginx实现反向代理;Nginx实现负载均衡;Nginx实现动静分离;Nginx实现高可用集群;Nginx4种常见配置实例如下:一、Nginx反向代理配...

使用nginx+allure管理自动化测试报告

allure在自动化测试中经常用来生成漂亮的报告,但是网上及官网上给出的例子都仅仅是针对单个测试用例文件的形式介绍的,实际使用中,自动化测试往往需要包含不止一个产品或项目,本文介绍如何使用nginx+...

nginx配置文件详解(nginx配置文件详解高清版)

Nginx是一个强大的免费开源的HTTP服务器和反向代理服务器。在Web开发项目中,nginx常用作为静态文件服务器处理静态文件,并负责将动态请求转发至应用服务器(如Django,Flask,et...

SpringCloud Eureka-服务注册与发现

1.Eureka介绍1.1学习Eureka前的说明目前主流的服务注册&发现的组件是Nacos,但是Eureka作为老牌经典的服务注册&发现技术还是有必要学习一下,原因:(1)一些早期的分布式微服...

微服务 Spring Cloud 实战 Eureka+Gateway+Feign+Hystrix

前言我所在项目组刚接到一个微服务改造需求,技术选型为SpringCloud,具体需求是把部分项目使用SpringCloud技术进行重构。本篇文章中介绍了Eureka、Gateway、Fe...

深度剖析 Spring Cloud Eureka 底层实现原理

你作为一名互联网大厂后端技术开发人员,在构建分布式系统时,是不是常常为服务的注册与发现而头疼?你是否好奇,像SpringCloudEureka这样被广泛使用的组件,它的底层实现原理到底是怎样的...

热爱生活,喜欢折腾。(很热爱生活)

原文是stackoverflow的一则高票回答,原文链接可能之前也有人翻译过,但是刚好自己也有疑惑,所以搬运一下,个人水平有限所以可能翻译存在误差,欢迎指正(如侵删)。尽管classmethod和st...

GDB调试的高级技巧(详细描述gdb调试程序的全过程)

GDB是我们平时调试c/c++程序的利器,查起复杂的bug问题,比打印大法要好得多,但是也不得不说,gdb在默认情况下用起来并不是很好用,最近学习到几个高级点的技巧,分享下:一美化打印先上个例子...

Arduino 实例(二十三)Arduino 给Python 编译器发送信息

1首先Python需要安装Pyserial库,在命令提示符中输入pipintallpyserial若是遇到提示‘pip‘不是内部或外部命令,也不是可运行的程序或批处理文件,则需要设置环境变...