1. 개요

이 예제에서는 JNI(Java Native Interface) 코드 를 작성하지 않고 기본 라이브러리에 액세스하기 위해 Java Native Access 라이브러리(줄여서 JNA)를 사용하는 방법을 볼 것 입니다.

2. 왜 JNA인가?

수년 동안 Java 및 기타 JVM 기반 언어는 "한 번 작성, 모든 곳에서 실행"이라는 모토를 상당 부분 충족했습니다. 그러나 때로는 일부 기능을 구현하기 위해 네이티브 코드를 사용해야 합니다 .

  • C/C++ 또는 네이티브 코드를 생성할 수 있는 다른 언어로 작성된 레거시 코드 재사용
  • 표준 Java 런타임에서 사용할 수 없는 시스템별 기능에 액세스
  • 주어진 애플리케이션의 특정 섹션에 대한 속도 및/또는 메모리 사용량 최적화.

처음에 이러한 종류의 요구 사항은 JNI(Java Native Interface)에 의존해야 한다는 것을 의미했습니다. 이 접근 방식은 효과적이지만 단점이 있으며 일반적으로 다음과 같은 몇 가지 문제로 인해 사용하지 않습니다.

  • 개발자가 Java와 기본 코드를 연결하기 위해 C/C++ "접착 코드"를 작성해야 함
  • 모든 대상 시스템에 사용할 수 있는 전체 컴파일 및 링크 도구 체인 필요
  • JVM에서 값을 마샬링 및 비정렬화하는 것은 지루하고 오류가 발생하기 쉬운 작업입니다.
  • Java와 기본 라이브러리를 혼합할 때의 법률 및 지원 문제

JNA는 JNI 사용과 관련된 대부분의 복잡성을 해결하기 위해 왔습니다. 특히 동적 라이브러리에 있는 네이티브 코드를 사용하기 위해 JNI 코드를 생성할 필요가 없으므로 전체 프로세스가 훨씬 쉬워집니다.

물론 다음과 같은 장단점이 있습니다.

  • 정적 라이브러리를 직접 사용할 수 없습니다.
  • 손으로 만든 JNI 코드와 비교할 때 느림

그러나 대부분의 응용 프로그램에서 JNA의 단순성 이점은 이러한 단점보다 훨씬 큽니다. 따라서 매우 구체적인 요구 사항이 없는 한 오늘날 JNA는 Java 또는 다른 JVM 기반 언어의 기본 코드에 액세스하는 데 가장 적합한 선택이라고 말할 수 있습니다.

3. JNA 프로젝트 설정

JNA를 사용하기 위해 가장 먼저 해야 할 일은 프로젝트의 pom.xml에 의존성을 추가하는 것입니다 .

<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna-platform</artifactId>
    <version>5.6.0</version>
</dependency>

최신 버전의  jna-platform 은 Maven Central에서 다운로드할 수 있습니다.

4. JNA 사용

JNA 사용은 2단계 프로세스입니다.

  • 먼저 대상 네이티브 코드를 호출할 때 사용되는 메서드와 유형을 설명하기 위해 JNA의 라이브러리 인터페이스를 확장하는 Java 인터페이스를 만듭니다.
  • 다음으로 이 인터페이스를 JNA에 전달하여 기본 메서드를 호출하는 데 사용하는 이 인터페이스의 구체적인 구현을 반환합니다.

4.1. C 표준 라이브러리에서 메소드 호출하기

첫 번째 예에서는 JNA를 사용하여 대부분의 시스템에서 사용할 수 있는 표준 C 라이브러리에서 cosh 함수 를 호출해 보겠습니다  . 이 메서드는 이중 인수를 사용하여 쌍곡선 코사인을 계산합니다 . AC 프로그램은 <math.h> 헤더 파일 을 포함하는 것만으로 이 기능을 사용할 수 있습니다 .

#include <math.h>
#include <stdio.h>
int main(int argc, char** argv) {
    double v = cosh(0.0);
    printf("Result: %f\n", v);
}

이 메서드를 호출하는 데 필요한 Java 인터페이스를 만들어 보겠습니다.

public interface CMath extends Library { 
    double cosh(double value);
}

다음으로 JNA의  Native 클래스를 사용하여 이 인터페이스의 구체적인 구현을 생성하여 API를 호출할 수 있습니다.

CMath lib = Native.load(Platform.isWindows()?"msvcrt":"c", CMath.class);
double result = lib.cosh(0);

여기서 정말 흥미로운 부분은 load()  메서드에 대한 호출  입니다. 동적 라이브러리 이름과 우리가 사용할 메소드를 설명하는 Java 인터페이스의 두 가지 인수가 필요합니다. 이 인터페이스의 구체적인 구현을 반환하여 해당 메서드를 호출할 수 있습니다.

이제 동적 라이브러리 이름은 일반적으로 시스템에 따라 다르며 C 표준 라이브러리도 예외는 아닙니다. 대부분의 Linux 기반 시스템에서는 libc.so 이지만  Windows 에서는 msvcrt.dll 입니다. 이것이 우리가 실행 중인 플랫폼을 확인하고 적절한 라이브러리 이름을 선택하기 위해 JNA에 포함된 Platform 헬퍼 클래스를 사용하는 이유 입니다.

 암시된 대로 .so 또는 .dll 확장자 를 추가할 필요가 없습니다 . 또한 Linux 기반 시스템의 경우 공유 라이브러리의 표준인 "lib" 접두사를 지정할 필요가 없습니다.

동적 라이브러리 는 Java 관점에서 싱글톤 처럼 작동하므로 일반적인 관행은 인터페이스 선언의 일부로 INSTANCE 필드 를 선언하는 것입니다 .

public interface CMath extends Library {
    CMath INSTANCE = Native.load(Platform.isWindows() ? "msvcrt" : "c", CMath.class);
    double cosh(double value);
}

4.2. 기본 유형 매핑

초기 예제에서 호출된 메서드는 기본 유형만 인수와 반환 값으로 사용했습니다. JNA는 일반적으로 C 유형에서 매핑할 때 자연적인 Java 대응물을 사용하여 이러한 경우를 자동으로 처리합니다.

  • 문자 => 바이트
  • 짧다 => 짧다
  • wchar_t => 문자
  • 정수 => 정수
  • long => com.sun.jna.NativeLong
  • 길다 => 길다
  • 플로트 => 플로트
  • 더블 => 더블
  • 문자 * => 문자열

이상해 보일 수 있는 매핑은 기본 long 유형에 사용되는 매핑입니다 . 이는 C/C++에서 long 유형이 32비트 또는 64비트 시스템에서 실행 중인지 여부에 따라 32비트 또는 64비트 값을 나타낼 수 있기 때문  입니다.

이 문제를 해결하기 위해 JNA는 시스템 아키텍처에 따라 적절한 유형을 사용하는 NativeLong 유형을 제공합니다 .

4.3. 구조 및 조합

또 다른 일반적인 시나리오는 일부 구조체 또는 공용체 유형에 대한 포인터를 예상하는 기본 코드 API를 처리하는 것  입니다. Java 인터페이스를 생성하여 액세스할 때 해당 인수 또는 반환 값은 각각 Structure 또는 Union 을 확장하는 Java 유형이어야 합니다 .

예를 들어, 이 C 구조체가 주어지면:

struct foo_t {
    int field1;
    int field2;
    char *field3;
};

Java 피어 클래스는 다음과 같습니다.

@FieldOrder({"field1","field2","field3"})
public class FooType extends Structure {
    int field1;
    int field2;
    String field3;
};

JNA는 @FieldOrder 어노테이션이 필요 하므로 대상 메소드에 대한 인수로 사용하기 전에 데이터를 메모리 버퍼로 적절하게 직렬화할 수 있습니다.

또는 동일한 효과에 대해 getFieldOrder() 메서드를 재정의할 수 있습니다 . 단일 아키텍처/플랫폼을 대상으로 하는 경우 전자의 방법이 일반적으로 충분합니다. 후자를 사용하여 플랫폼 간 정렬 문제를 처리할 수 있으며, 때로는 추가 패딩 필드를 추가해야 합니다.

노동 조합 은 몇 가지 점을 제외하고 유사하게 작동합니다.

  • @FieldOrder 어노테이션 을 사용 하거나 getFieldOrder()를 구현할 필요가 없습니다.
  • 우리는 전화로이  setType () 네이티브 메소드를 호출하기 전에

간단한 예를 통해 수행하는 방법을 살펴보겠습니다.

public class MyUnion extends Union {
    public String foo;
    public double bar;
};

이제 가상 라이브러리와 함께 MyUnion사용하겠습니다 .

MyUnion u = new MyUnion();
u.foo = "test";
u.setType(String.class);
lib.some_method(u);

foobar모두  같은 유형인 경우 필드 이름을 대신 사용해야 합니다.

u.foo = "test";
u.setType("foo");
lib.some_method(u);

4.4. 포인터 사용

JNA는 형식화되지 않은 포인터(일반적으로 void * )로 선언된 API를 처리하는 데 도움이 되는 포인터 추상화를 제공합니다  . 이 클래스는 명백한 위험이 있는 기본 기본 메모리 버퍼에 대한 읽기 및 쓰기 액세스를 허용하는 메서드를 제공합니다.

이 클래스를 사용하기 전에 매번 참조된 메모리를 "소유"하는 사람이 누구인지 명확하게 이해해야 합니다. 그렇게 하지 않으면 메모리 누수 및/또는 잘못된 액세스와 관련된 디버그하기 어려운 오류가 발생할 수 있습니다.

우리가 무엇을 하는지 알고 있다고 가정하고(항상 그렇듯이), 메모리 버퍼를 할당하고 해제하는 데 사용되는 잘 알려진 malloc()free() 함수를 JNA와 함께 사용하는 방법을 살펴보겠습니다 . 먼저 래퍼 인터페이스를 다시 생성해 보겠습니다.

public interface StdC extends Library {
    StdC INSTANCE = // ... instance creation omitted
    Pointer malloc(long n);
    void free(Pointer p);
}

이제 그것을 사용하여 버퍼를 할당하고 가지고 놀아봅시다:

StdC lib = StdC.INSTANCE;
Pointer p = lib.malloc(1024);
p.setMemory(0l, 1024l, (byte) 0);
lib.free(p);

setMemory () 메소드는 (이 경우에, 영)으로 일정한 바이트 값 기본 버퍼를 채운다. 주목하라 것을  포인터 인스턴스가 가리키는 것과 아무 생각, 더 적은 크기가 없습니다. 이것은 메소드를 사용하여 힙을 아주 쉽게 손상시킬 수 있음을 의미합니다.

JNA의 충돌 방지 기능을 사용하여 이러한 오류를 완화하는 방법은 나중에 살펴보겠습니다.

4.5. 오류 처리

표준 C 라이브러리의 이전 버전은 전역 errno 변수를 사용하여 특정 호출이 실패한 이유를 저장했습니다. 예를 들어, 이것은 일반적인 open() 호출이 C에서 이 전역 변수를 사용 하는 방법입니다  :

int fd = open("some path", O_RDONLY);
if (fd < 0) {
    printf("Open failed: errno=%d\n", errno);
    exit(1);
}

물론 현대의 다중 스레드 프로그램에서는 이 코드가 작동하지 않겠죠? 글쎄요, C의 전처리기 덕분에 개발자들은 여전히 ​​이와 같은 코드를 작성할 수 있고 잘 작동할 것입니다. 요즘  errno 는 함수 호출로 확장되는 매크로입니다.

// ... excerpt from bits/errno.h on Linux
#define errno (*__errno_location ())

// ... excerpt from <errno.h> from Visual Studio
#define errno (*_errno())

이제 이 접근 방식은 소스 코드를 컴파일할 때 잘 작동하지만 JNA를 사용할 때는 그런 것이 없습니다. 래퍼 인터페이스에서 확장된 함수를 선언하고 명시적으로 호출할 수 있지만 JNA는 더 나은 대안인 LastErrorException을 제공합니다 .

LastErrorException throws를 사용 하여 래퍼 인터페이스에 선언된 모든 메서드는 기본 호출 후 오류 검사를 자동으로 포함합니다. 오류를 보고하면 JNA는 원래 오류 코드를 포함 하는 LastErrorException을 발생시킵니다.

 이 기능을 실제로 표시하기 위해 이전에 사용한 몇 가지 메서드를 StdC 래퍼 인터페이스에 추가해 보겠습니다 .

public interface StdC extends Library {
    // ... other methods omitted
    int open(String path, int flags) throws LastErrorException;
    int close(int fd) throws LastErrorException;
}

이제 try/catch 절에서 open()사용할 수 있습니다  .

StdC lib = StdC.INSTANCE;
int fd = 0;
try {
    fd = lib.open("/some/path",0);
    // ... use fd
}
catch (LastErrorException err) {
    // ... error handling
}
finally {
    if (fd > 0) {
       lib.close(fd);
    }
}

에서  캐치 블록, 우리가 사용할 수 있습니다 LastErrorException.getErrorCode ()를 원래 얻을 의 errno 값을 에러 처리 로직의 일부로 사용합니다.

4.6. 액세스 위반 처리

앞서 언급했듯이 JNA는 주어진 API를 오용하는 것으로부터 우리를 보호하지 않습니다. 특히 네이티브 코드 앞뒤로 전달되는 메모리 버퍼를 다룰 때 그렇습니다 . 정상적인 상황에서는 이러한 오류로 인해 액세스 위반이 발생하고 JVM이 종료됩니다.

JNA는 Java 코드가 액세스 위반 오류를 처리할 수 있도록 하는 방법을 어느 정도 지원합니다. 활성화하는 방법에는 두 가지가 있습니다.

  • 설정  jna.protected 에 시스템 속성을 
  • Native.setProtected(true) 호출 

이 보호 모드를 활성화하면 JNA는 일반적으로 충돌을 일으키고 java.lang.Error 예외를 발생시키는 액세스 위반 오류를 포착합니다 . 유효하지 않은 주소로 초기화 포인터를 사용 하고 여기에 일부 데이터를 쓰려고 하면 이것이 작동하는지 확인할 수 있습니다  .

Native.setProtected(true);
Pointer p = new Pointer(0l);
try {
    p.setMemory(0, 100*1024, (byte) 0);
}
catch (Error err) {
    // ... error handling omitted
}

그러나 설명서에 나와 있는 것처럼 이 기능은 디버깅/개발 목적으로만 사용해야 합니다.

5. 결론

이 기사에서는 JNI와 비교할 때 JNA를 사용하여 네이티브 코드에 쉽게 액세스하는 방법을 보여주었습니다.

평소와 같이 모든 코드는 GitHub에서 사용할 수  있습니다 .

Junit footer banner