JNA使用教程

前言

JNA 全称 Java Native Access

JNA provides Java programs easy access to native shared libraries without writing anything but Java code - no JNI or native code is required.

除了 Java 代码你不需要编写任何其他代码即可完成本地方法调用不需要编写 jni 代码也不需要编写 java native 代码。

当然如果不是调用别人设计好的本地方法库也是需要自己编写本地方法库代码的,同时本地方法也需要少量的适配。本次案例我们会自己编写库代码

JNA还能帮我们自动完成JAVA <--> C 的类型映射

JAVA C 类型映射表

Native Type Size Java Type Common Windows Types
char 8-bit integer byte BYTE, TCHAR
char 8-bit integer byte BYTE, TCHAR
short 16-bit integer short WORD
wchar_t 16/32-bit character char TCHAR
int 32-bit integer int DWORD
int boolean value boolean BOOL
long 32/64-bit integer NativeLong LONG
long long 64-bit integer long __int64
float 32-bit FP float
double 64-bit FP double
char* C string String LPCSTR
void* pointer Pointer LPVOID, HANDLE, LPXXX

该表来自官方 Github repository

案例环境

Jdk 版本 "1.8.0_311"

IDE:

  • IDEA
  • Visual Studio 2022 Community

准备

创建普通的 Gradle 项目并添加 JNA 依赖

implementation 'net.java.dev.jna:jna:5.10.0'

当然 maven 项目也可以


    net.java.dev.jna
    jna
    5.10.0

创建 C++ 项目

在项目模板中选择须有导出项的(DLL)动态链接库创建新的项目


创建完成后一般会生成以下文件

framework.h

无特殊作用,引入 windows 头文件,在编译 linux so 库时我们会把他删掉

pch.h&pch.cpp

这个不是很懂直接复制其注释

// pch.h: 这是预编译标头文件。下方列出的文件仅编译一次,提高了将来生成的生成性能。这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。请勿在此处添加要频繁更新的文件,这将使得性能优势无效。

``

dllmain.cpp

DLL 入口文件,其中描述了 DLL 的几个生命周期。如果想在调用阶段的不同生命周期做相应的操作可在此进行(仅 Windows 下生效)

项目名称.h&项目名称.cpp

这两个文件一般是与创建项目的名称相同,其中项目名称.cpp中包含了几个导出函数和变量的实例。不过并不能直接使用后期我们会编写我们自己的导出项。后面所有导出项也会全部写在这里。

正文

假设创建的 C++项目名称为 ALMing

关于宏定义 XXX_API

ALMing.cpp

// Dll1.cpp : 定义 DLL 的导出函数。
//

#include "pch.h"
#include "framework.h"
#include "ALMing.h"


// 这是导出变量的一个示例
ALMING_API int nALMing=0;

// 这是导出函数的一个示例。
ALMING_API int fnALMing(void)
{
    return 0;
}

// 这是已导出类的构造函数。
ALMing::ALMing()
{
    return;
}

其中 ALMING_API 是在 ALMing.h 中的宏定义用来指明该函数/变量为导出项(仅 Windows 下有效)

#ifdef ALMING_EXPORTS
#define ALMING_API __declspec(dllexport)
#else
#define ALMING_API __declspec(dllimport)
#endif

后续编写的所有导出项都会以此开头。

第一个函数

ALMing.cpp中添加

extern "C"  ALMING_API const char* helloJna()
{
    return "Hello Jna";
}

函数的功能是返回一个字符串值。添加完成后将此项目编译为DLL文件,在 visual studio 中点击菜单栏 生成 > 生成解决方案 即可编译项目。编写生成的DLL文件可在项目根目录下 x64 > Debug 下找到。

在Java项目中新建一个接口并继承 JNA 库中的 Library 接口。

public interface CLibrary extends Library {
   CLibrary INSTANCE = Native.load("E:\\Projects\\C\\c-jna\\x64\\Debug\\ALMing", CLibrary.class);
   String helloJna();
}

该接口中声明一个与C++代码中的同名方法。(注意必须同名且参数和返回值与C++中反方法一一对应)对应类型的转换关系上文表中已给出,同时使用Native.load()方法加载本地方法库。

接下来就可以使用Native.load()创建的实例进行对C++方法的调用

public class App {
    public static void main(String[] args) {
        CLibrary lib = CLibrary.INSTANCE;
        String helloJna = lib.helloJna();
        System.out.println(helloJna);
    }
}

结果:

> Task :App.main()
DLL_PROCESS_ATTACH
Hello Jna
DLL_THREAD_ATTACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_PROCESS_DETACH

可以看到成功打印了Hello Jna,同时还打印了DLL_XXX_XXX。这里就是前文所提到的 dllmain.cpp 中的生命周期。我修改了dllmain.cpp的代码

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include 

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        std::cout << "DLL_PROCESS_ATTACH" << std::endl;
        break;
    case DLL_THREAD_ATTACH:
        std::cout << "DLL_THREAD_ATTACH" << std::endl;
        break;
    case DLL_THREAD_DETACH:
        std::cout << "DLL_THREAD_DETACH" << std::endl;
        break;
    case DLL_PROCESS_DETACH:
        std::cout << "DLL_PROCESS_DETACH" << std::endl;
        break;
    }
    return TRUE;
}
可以看出打印出的信息都是符合预期的。

关于 extern "C"

所有导出给JNA使用的函数接需要使用 extern "C" 进行声明。否则Java调用时会找不到此函数。这里有两种声明方式。

  1. 编写所有函数时均添加 extern "C" 前缀
extern "C"  ALMING_API const char* fun1()
{
    return "fun1";
}
extern "C"  ALMING_API const char* fun2()
{
    return "fun2";
}
  1. 声明一个 extern "C" 并将所有需要导出的函数包含在此代码块中
extern "C" 
{
    ALMING_API const char* fun1()
    {
        return "fun1";
    }
    ALMING_API const char* fun2()
    {
        return "fun2";
    }
}

再编写一个带参数的导出函数

这次使用 extern "C" 块形式声明

extern "C" 
{

    ALMING_API int add(int a, int b)
    {
        return a + b;
    }
}

同理Java中声明相同签名的函数

public interface CLibrary extends Library {
   CLibrary INSTANCE = Native.load("E:\\Projects\\C\\c-jna\\x64\\Debug\\ALMing", CLibrary.class);
   String helloJna();
   int add(int a, int b);
}

可以看出,Java中需要做的就是声明一个与C++中导出方法相同签名(准确来说是类似签名)的方法。其中参数和返回值类型的映射关系都可在上文中表中找到。除了bool 类型看起来比较特殊。

如何调用 C++ 类中的方法

相信这才是大多数实用场景,当然如果调用的是纯C的库例外。我也是在工作中遇到此需求才来学习的JNA。

先创建一个C++类,该类为一个程序员类拥有姓名,年龄,性别,发量属性

//Programmer.h
#pragma once

class CProgrammer
{
private:
    char* m_name;
    int m_age;
    bool m_sex;
    int m_hairAmount;
public:
    CProgrammer();
    CProgrammer(char* name, int age,bool sex,int hairAmount);
    char* getName();
    void setName(char* name);
    int getAge();
    void setAge(int age);
    bool getSex();
    void setSex(int sex);
    int getHairAmount();
    void setHairAmount(int hairAmount);
    void introduce();
};

introduce() 方法打印了自我介绍信息

//Programmer.cpp
#include "pch.h"
#include "Programmer.h"
#include 

using namespace std;

CProgrammer::CProgrammer()
{
}

CProgrammer::CProgrammer(char* name, int age, bool sex, int hairAmount)
    :m_name((char*)""),m_age(18),m_sex(true),m_hairAmount(0)
{
    m_name = name;
    m_age = age;
    m_sex = sex;
    m_hairAmount = hairAmount;
}

char* CProgrammer::getName()
{
    return m_name;
}

void CProgrammer::setName(char* name)
{
    m_name = name;
}

int CProgrammer::getAge()
{
    return m_age;
}

void CProgrammer::setAge(int age)
{
    m_age = age;
}

bool CProgrammer::getSex()
{
    return m_sex;
}

void CProgrammer::setSex(int sex)
{
    m_sex = sex;
}

int CProgrammer::getHairAmount()
{
    return m_hairAmount;
}

void CProgrammer::setHairAmount(int hairAmount)
{
    m_hairAmount = hairAmount;
}

void CProgrammer::introduce()
{
    cout << "Hello I'm " + string(m_name)+" ";
    cout << m_age;
    cout << " years old.";
    cout << "I have ";
    cout << m_hairAmount;
    cout << " hairs" << endl;

}

首先想要调用C++类中的方法必须拥有一个该类的实例才可以。那么我们首先通过JNA创建一个C++类的实例

extern "C" 
{
    ALMING_API CProgrammer* newProgrammer(char* name,int age,bool sex,int hairAmount) 
    {
        return new CProgrammer(name,age,sex,hairAmount);
    }
}

那么可以看到其返回值类型是 CProgrammer 类型的指针,而上文类型映射表中仅给出了基本类型的映射关系。JNA库提供了一个 Pointer 类用来映射所有指针类型。所以Java中的方法签名应为

Pointer newProgrammer(String name, int age,boolean sex,int hairAmount);

我们拿到了该类实例的指针地址,接下来就可以声明一个函数接收该指针地址并使用该实例指针进行方法调用了

extern "C" 
{
   ALMING_API void introduce(CProgrammer *programmer)
    {
        programmer->introduce();
    }
}

同理在Java中声明 introduce 方法

void introduce(Pointer pointer);

接下来在 Java 中进行调用测试

public class App {
    public static void main(String[] args) {
        CLibrary lib = CLibrary.INSTANCE;
        Pointer programmer = lib.newProgrammer("alming", 18, true, 180);
        lib.introduce(programmer);
    }
}

结果:

Task :App.main()
DLL_PROCESS_ATTACH
Hello I'm alming 18 years old.I have 180 hairs
DLL_THREAD_ATTACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_PROCESS_DETACH

使用结构体

前文中指针类型JNA中使用 Pointer 映射。C 结构体类型可映射成为Java类但被映射类必须继承 com.sun.jna.Structure 类型并使用 @Structure.FieldOrder 注解标注各属性的名称及顺序或重写getFieldOrder()方法返回与该类属性对应的属性名称列表,同时所有属性均需声明为 public

接下来我们先定义一个结构体 Computer

typedef struct {
    char* brand;
    char*  name;
    char*  cpu;
    char*  gpu;
}Computer;

Programmer类添加 computer 属性

class CProgrammer
{
private:
    char* m_name;
    int m_age;
    bool m_sex;
    int m_hairAmount;
    Computer m_computer;
public:
    CProgrammer();
    CProgrammer(char* name, int age,bool sex,int hairAmount);
    char* getName();
    void setName(char* name);
    int getAge();
    void setAge(int age);
    bool getSex();
    void setSex(int sex);
    int getHairAmount();
    void setHairAmount(int hairAmount);
    void introduce();
    Computer getComputer();
    void setComputer(Computer computer);
};

在setComputer中打印下结构体信息以便验证结构体数据是否成功从Java测传递过来。

//Programmer.cpp
Computer CProgrammer::getComputer()
{
    return m_computer;
}

void CProgrammer::setComputer(Computer computer)
{
    cout << computer.brand << endl;
    cout << computer.name << endl;
    cout << computer.cpu << endl;
    cout << computer.gpu << endl;
    m_computer = computer;
}

创建导出函数

extern "C" 
{
    ALMING_API void setComputer(CProgrammer* programer, Computer* computer)
    {
        programer->setComputer(computer);
    }
    ALMING_API Computer* getComputer(CProgrammer* programer)
    {
       return programer->getComputer();
    }
}

Java侧映射

//Computer.java
@Structure.FieldOrder({"brand", "name", "cpu", "gpu"})
public class Computer extends Structure {
    public String brand;
    public String name;
    public String cpu;
    public String gpu;
    
    public Computer() {
    }
    public Computer(String brand, String name, String cpu, String gpu) {
        this.brand = brand;
        this.name = name;
        this.cpu = cpu;
        this.gpu = gpu;
    }
    // @Structure.FieldOrder({"brand", "name", "cpu", "gpu"}) 和下面方法存在一个即可
    // @Override
    // protected List getFieldOrder() {
    //     List fields = new ArrayList<>();
    //     fields.add("brand");
    //     fields.add("name");
    //     fields.add("cpu");
    //     fields.add("gpu");
    //     return fields;
    // }

    @Override
    public String toString() {
        return "Computer{" +
                "brand='" + brand + '\'' +
                ", name='" + name + '\'' +
                ", cpu='" + cpu + '\'' +
                ", gpu='" + gpu + '\'' +
                '}';
    }
}
public interface CLibrary extends Library {
   CLibrary INSTANCE = Native.load("E:\\Projects\\C\\c-jna\\x64\\Debug\\ALMing", CLibrary.class);
   Pointer newProgrammer(String name, int age,boolean sex,int hairAmount);
   Pointer setComputer(Pointer pointer,Computer computer);
   Computer getComputer(Pointer pointer);
}

调用测试

public class App {
    public static void main(String[] args) {
        CLibrary lib = CLibrary.INSTANCE;
        Pointer programmer = lib.newProgrammer("alming", 18, true, 180);
        Computer computer = new Computer("HUAWEI", "MATE", "R7 4800H", "GTX 2060");
        Pointer newProgrammer = lib.setComputer(programmer, computer);
        Computer getFromC = lib.getComputer(programmer);
        System.out.println(getFromC);
    }
}

结果:

> Task :App.main()
DLL_PROCESS_ATTACH
HUAWEI
MATE
R7 4800H
GTX 2060
Computer{brand='HUAWEI', name='MATE', cpu='R7 4800H', gpu='GTX 2060'}
DLL_THREAD_ATTACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_PROCESS_DETACH

结构体返回到Java数据值对不上问题

上面情况并不会出现数值对不上的情况。当出现如下场景时,返回值通常会对不上

  • C代码中直接声明 结构体变量并为其赋值后返回
  • 结构体成员是数值类型

这里我只测试了double 和 int 这两种结构体成员类型。这两种类型按如上返回方式会导致Java中数值错误现象。char* 类型不会。其余类型如有需要可自行测试。

下面我们使用 int 创建一种错误返回的实例

首先修改结构体定义,将 char* 改为 int

typedef struct {
    int brand;
    int name;
    int cpu;
    int gpu;
}Computer;

然后创建一个导出函数

extern "C" 
{
    ALMING_API Computer* buyComputer()
    {
        Computer computer;
        computer.brand = 12;
        computer.name = 5;
        computer.cpu = 8;
        computer.gpu = 7;
        return &computer;
    }
}

同样在Java中添加映射

public interface CLibrary extends Library {
   CLibrary INSTANCE = Native.load("E:\\Projects\\C\\c-jna\\x64\\Debug\\ALMing", CLibrary.class);
   Computer buyComputer();
}

添加测试方法

public class App {
    public static void main(String[] args) {
        CLibrary lib = CLibrary.INSTANCE;
        Computer computer = lib.buyComputer();
        System.out.println("------------------");
    }
}

以Debug模式调试该代码

可以看到并未得到预期的结果

那么如何解决,解决方式非常简单C++在声明computer变量时使用 new 关键字如下

extern "C" 
{
    ALMING_API Computer* buyComputer()
    {
        Computer* computer = new Computer();
        computer->brand = 12;
        computer->name = 5;
        computer->cpu = 8;
        computer->gpu = 7;
        return computer;
    }
}

其实本身这也是我编程的失误导致的,如果使用 visual studio 直接声明变量 ide 是会报变量未初始化警告的。虽然在C中它是可以编译并运行的。

好了现在解决了问题我们重新Debug下Java程序。


结果成功返回!

自定义类型映射

Native Type Size Java Type Common Windows Types
int boolean value boolean BOOL

我们从类型映射表中可以看到,Java 的 boolean 被映射成为 C 的 int 类型。如果你运行过那么你会发现 true 会被映射成为 -1 false 则为 0,但这没关系。这并不影响 C 对于逻辑的判断。因为C可以使用int进行逻辑判断认为 0 为 false 非零的任何数值都会被认为时true。

但有些时候我们希望它会按照我们的直觉 true 为 1 false 为 0 。或许有些场景就需要这样严格的标准或是仅仅是强迫症,不妨我们使用C 的 bool 类型来映射Java boolean来试一试。编写测试方法

ALMING_API void testBool(bool is) 
{
    if (is)
    {
        cout << "TRUE" << endl;
    }
    cout << is<

在函数中我们输出了is 变量的值。首先我们需要知道。cout API 并不会为我们输出 true 或 false 这种自然语言标识而是会输出is这个一字节数据所表示的十进制数。然后当你真的运行过它你会得到如下结论:true 映射成为 255,false 被映射成为 0。从二进制的角度我们似乎得到了一个很合理的答案,因为 255 说明这个一字节的数据的 8 位被填满了1 而 0 则是八位全为 0。

那么如果我偏想完成 true ->1 false->0 十进制角度的映射呢。JNA为我们提供了自定义类型映射的能力。在JNA官方Github仓库下W32APITypeMapper.java就是一个自定义类型映射的示例。其中就有一个 bool 类型的 TypeConverter。把它拷贝到我的项目下。

// W32APITypeMapper.java
/* Copyright (c) 2007 Timothy Wall, All Rights Reserved
 *
 * The contents of this file is dual-licensed under 2
 * alternative Open Source/Free licenses: LGPL 2.1 or later and
 * Apache License 2.0. (starting with JNA version 4.0.0).
 *
 * You can freely decide which license you want to apply to
 * the project.
 *
 * You may obtain a copy of the LGPL License at:
 *
 * http://www.gnu.org/licenses/licenses.html
 *
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "LGPL2.1".
 *
 * You may obtain a copy of the Apache License at:
 *
 * http://www.apache.org/licenses/
 *
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "AL2.0".
 */
package com.sun.jna.win32;

import com.sun.jna.DefaultTypeMapper;
import com.sun.jna.FromNativeContext;
import com.sun.jna.StringArray;
import com.sun.jna.ToNativeContext;
import com.sun.jna.TypeConverter;
import com.sun.jna.TypeMapper;
import com.sun.jna.WString;

/** Provide standard conversion for W32 API types.  This comprises the
 * following native types:
 * 
    *
  • Unicode or ASCII/MBCS strings and arrays of string, as appropriate *
  • BOOL *
* @author twall */ public class W32APITypeMapper extends DefaultTypeMapper { /** Standard TypeMapper to use the unicode version of a w32 API. */ public static final TypeMapper UNICODE = new W32APITypeMapper(true); /** Standard TypeMapper to use the ASCII/MBCS version of a w32 API. */ public static final TypeMapper ASCII = new W32APITypeMapper(false); /** Default TypeMapper to use - depends on the value of {@code w32.ascii} system property */ public static final TypeMapper DEFAULT = Boolean.getBoolean("w32.ascii") ? ASCII : UNICODE; protected W32APITypeMapper(boolean unicode) { if (unicode) { TypeConverter stringConverter = new TypeConverter() { @Override public Object toNative(Object value, ToNativeContext context) { if (value == null) return null; if (value instanceof String[]) { return new StringArray((String[])value, true); } return new WString(value.toString()); } @Override public Object fromNative(Object value, FromNativeContext context) { if (value == null) return null; return value.toString(); } @Override public Class nativeType() { return WString.class; } }; addTypeConverter(String.class, stringConverter); addToNativeConverter(String[].class, stringConverter); } TypeConverter booleanConverter = new TypeConverter() { @Override public Object toNative(Object value, ToNativeContext context) { return Integer.valueOf(Boolean.TRUE.equals(value) ? 1 : 0); } @Override public Object fromNative(Object value, FromNativeContext context) { return ((Integer)value).intValue() != 0 ? Boolean.TRUE : Boolean.FALSE; } @Override public Class nativeType() { // BOOL is 32-bit int return Integer.class; } }; addTypeConverter(Boolean.class, booleanConverter); } }

现在有了TypeMapper,我们需要在创建Java Library 接口实例时告诉 JNA我使用自定义的类型映射。所以按如下方式改写获取实例的方法:

public interface CLibrary extends Library {
   // CLibrary INSTANCE = Native.load("E:\\Projects\\C\\c-jna\\x64\\Debug\\ALMing", CLibrary.class);
   static CLibrary getInstance() {
      Map options = new HashMap<>();
      options.put(Library.OPTION_TYPE_MAPPER,W32APITypeMapper.ASCII);
      return Native.load("E:\\Projects\\C\\c-jna\\x64\\Debug\\ALMing", CLibrary.class,options);
   }
   void testBool(boolean is);
}

随后运行测试

public class App {
    public static void main(String[] args) {
        CLibrary lib = CLibrary.getInstance();
        System.out.println("When value is true");
        lib.testBool(true);
        System.out.println("When value is false");
        lib.testBool(false);
        System.out.println("------------------");
    }
}

结果:

> Task :App.main()
DLL_PROCESS_ATTACH
When value is true
TRUE
1
When value is false
0
------------------
DLL_THREAD_ATTACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_PROCESS_DETACH

可以看到是符合预期的

使用回调

有时我们需要在本地方法中完成某项操作完成后需要通知 Java 端,此时多半是要用到回调了。接下将介绍如何完成 C 对 Java 的回调

首先 C 程序端需要声明一个 函数类型指针当作回调函数,然后导出一个函数其参数即为改 函数指针类型。即这个函数类型指针将会绑定到 Java 侧

extern "C" 
{
    typedef void (* SongNameCallback)(const char* callbackParam);

    ALMING_API void sing(SongNameCallback callback)
    {
        callback("God is a girl");
    }
}

在Java侧声明一个接口并继承 JNA的 Callback接口。然后声明一个方法其参数与 C 程序端指针函数的参数相匹配。方法名任意。最后创建绑定函数接收一个此接口类型变量的方法。

public interface CLibrary extends Library {
   CLibrary INSTANCE = Native.load("E:\\Projects\\C\\c-jna\\x64\\Debug\\ALMing", CLibrary.class);
   interface ALMingCallback extends Callback{
      void invoke(String name);
   }
   void sing(ALMingCallback fn);
}

最后来测试下:

public class App {
    public static void main(String[] args) {
        CLibrary lib = CLibrary.INSTANCE;
        CLibrary.ALMingCallback callback = name -> System.out.println("She sing: " + name);
        lib.sing(callback);
    }
}

结果:

> Task :App.main()
DLL_PROCESS_ATTACH
------------------
She sing: God is a girl
DLL_THREAD_ATTACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_THREAD_DETACH
DLL_PROCESS_DETACH

可以看到,回调成功被调用并且获取到了 C 程序端传回的参数。

Windows 下调试

上文主要介绍了如何使用 JNA 让Java调用 C 代码。接下来介绍下如何在Window下调试 JNA 的 C 代码。

首先在Java代码将要调试的代码片段前任意位置加一个断点,其目的是为了不让程序启动后直接调用到 C 代码。

public class App {
    public static void main(String[] args) {
        CLibrary lib = CLibrary.getInstance();
->      System.out.println("------------------");
        Pointer programmer = lib.newProgrammer("alming", 18, true, 180);
        lib.introduce(programmer);

    }
}

Debug该程序,示例程序中会在 System.out.println 触发断点,此时代开命令(系统的任意命令行)执行 jps 命令,执行后会得到如下结果,其内容为你的机器上正在运行的Java程序。第一行即为对应程序的 PID 。

16608 GradleDaemon
7024 Jps
18852 RemoteMavenServer36
20100
21832 App
13948 GradleDaemon

找到运行程序的 PID 例如示例程序为App 那么其 PID 为 21832,在visual studio 中找到调试按钮点击附加到进程

在弹出的页面找到对应 PID 的进程点击附加即可。此时我们的 visual studio 也进入了 Debug 模式在需要调试的地方打上端点然后就可以在Java程序中继续运行程序了,直至调用到 JNA 代码。

Linux 下的JNA

准备

  • 一台 linux 机器(虚拟机即可)

    注:最好使用有图形界面的系统,因为后期在命令行中调试Java程序是一件非常麻烦的事情。如果实在没有建议使用远程调试功能。

  • 将 Java 项目拷贝到linux系统中

  • 将 Visual Studio 项目的所有 C 相关代码拷贝到 Linux 中(*.h *.c *.cpp)

  • 准备开发环境,示例环境使用的是 Ubuntu 20.04.1 LTS,Java示例程序使用的IDE依然是 IDEA。C 开发环境使用的 build-essential 可以认为是C开发软件的合集。直接使用 sudo apt install build-essential 即可安装。

注:如果是 Centos 系统没有 build-essential 软件包 可以分别安装 gcc g++ make gdb 等 C 编译开发工具

修改 C 代码

由于使用Visual Studio 创建的项目有很多 window 的平台代码在C中是无法编译的。所有我们要删除所有的Windows平台代码

首先看下拷贝过来的目录结构

alming@ALMing:~/Projects/c-jna$ ls -l
total 32
-rwxrwxr-x 1 alming alming 2195 5月   2 21:36 ALMing.cpp
-rwxrwxr-x 1 alming alming  750 4月  30 09:26 ALMing.h
-rwxrwxr-x 1 alming alming  740 4月  30 09:26 dllmain.cpp
-rwxrwxr-x 1 alming alming  159 4月  30 09:26 framework.h
-rwxrwxr-x 1 alming alming  158 4月  30 09:26 pch.cpp
-rwxrwxr-x 1 alming alming  544 4月  30 09:26 pch.h
-rwxrwxr-x 1 alming alming 1316 5月   2 20:20 Programmer.cpp
-rwxrwxr-x 1 alming alming  677 5月   2 20:29 Programmer.h

首先 pch.h pch.cpp dllmain.cpp framework.h 都需要删除。其次就是删除其余文件中对这几个文件的引用的代码。

例如在ALMing.cpp中删除 #include "pch.h"#include "framework.h"

// ALMing.cpp : 定义 DLL 的导出函数。
//

//#include "pch.h"     
//#include "framework.h"
#include "ALMing.h"
#include "Programmer.h"
#include 

如果你使用 VSCode 并且安装了 C/C++ Extenstion 很容易发现这些引用,因为当删除了这些文件后 IDE 会有引用错误提示

找到 Windows 的导出宏定义删除它。示例代码它在 ALMing.h 中

// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 ALMING_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// ALMING_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。
//#ifdef ALMING_EXPORTS
//#define ALMING_API __declspec(dllexport)
//#else
//#define ALMING_API __declspec(dllimport)
//#endif

找到所有使用了改宏定义的位置删除引用。主要导出函数的那个文件。示例代码大部分在 ALMing.cpp中

至此linux代码就算是移植完成了,随后就可以使用 g++ *.h *.cpp -shared -g -fPIC -o ALMing.so 命令编译生成你的代码。

可选的可以在根目录创建一个 Makefile 文件然后输入如下内容

makeso:
    g++ *.h *.cpp -shared -g -fPIC -o ALMing.so

这样每次编译可以直接在命令行执行 make 命令即可,不用输入完整的 编译命令。

Java 中使用同 Windows 基本一致,只不过 Linux 库文件名称需要写后缀名如下

public interface CLibrary extends Library {
   CLibrary INSTANCE = Native.load("/home/alming/Projects/c-jna/ALMing.so", CLibrary.class);
}

Linux 下调试

同 Windows 下调试一样。Linux下调试首先需要获取运行 Java 程序的 PID。

然后为了方便 Linux 切换到root用户下(使用 gdb attach 到其他进程需要拥有 root 权限)。执行 gdb 命令进入 gdb 命令行。

使用 attach PID 附加到正在运行的 Java 进程中就可以使用 gdb相关命令进行调试了。

(gdb) attach 4604
Attaching to process 4604
[New LWP 4607]
[New LWP 4611]
[New LWP 4612]
[New LWP 4613]
[New LWP 4614]
[New LWP 4615]
[New LWP 4616]
[New LWP 4617]
[New LWP 4618]
[New LWP 4619]
[New LWP 4621]
[New LWP 4623]
[New LWP 4625]
[New LWP 4626]
[New LWP 4627]
[New LWP 4628]
[New LWP 4629]
[New LWP 4631]
warning: Could not load shared library symbols for /home/alming/.cache/JNA/temp/jna1982625280334040105.tmp.
Do you need "set solib-search-path" or "set sysroot"?
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
--Type  for more, q to quit, c to continue without paging--
__pthread_clockjoin_ex (threadid=139754756859648, thread_return=0x7ffd2698ffc8, 
    clockid=, abstime=, block=)
    at pthread_join_common.c:145
145     pthread_join_common.c: No such file or directory.
(gdb) b newProgrammer
Breakpoint 1 at 0x7f1b300ab482: file ALMing.cpp, line 28.
(gdb) 

关于如何使用 gdb 就要交给搜索引擎了。例如 b 打断点 l 查看源码 p 打印变量值 n 下一步 s 下一步如果是函数调用且能够进入则进入该函数。

这里值得注意的是当 so 库被调用时进入的并不是 我们写的代码。所以我们需要在我们的代码上打上断点后执行 c 让程序执行到我们的代码。刚进入调试时 行号断点是不可用的。因为调试开始并不是我们写的代码,同时使用 文件名:行号也会提示无法找到文件所以推荐使用函数名断点。这里想表达的是 attach 到我们的进程上后可以直接进行调试使用函数名断点即可。因为当时我遇到 warning: Could not load shared library symbols for /home/alming/.cache/JNA/temp/jna1982625280334040105.tmp.这样的警告。这使我本能的任务没加载到符号调试肯定没法进行,其实不然。行动起来就好打上断点直接开搞。

最后


大幻梦森罗万象狂气断罪眼\ (•◡•) /

你可能感兴趣的:(JNA使用教程)