레이블이 cmake인 게시물을 표시합니다. 모든 게시물 표시
레이블이 cmake인 게시물을 표시합니다. 모든 게시물 표시

2018년 6월 22일 금요일

[JNI] JNI로 C와 Java 연동하기

개요


Java를 사용하면 하드웨어나 OS의 대부분 기능을 제한 없이 사용할 수 있지만 그래도 C와 같은 저수준의 언어보다 섬세할 수는 없다. 이를 해소하기 위해 Java와 C/C++ 사이에 인터페이스를 제공하는 것이 JNI(Java Native Interface)이다.

주로 JNI는 다음과 같은 이유로 사용된다.
1. 이미 C/C++로 작성된 라이브러리를 Java에서 활용하기 위해
2. 처리속도 향상을 위해
3. 하드웨어나 OS의 기능 중 Java에서 아직 제공하지 않는 기능을 사용하기 위해


참고 자료


The Java Native Interface: Programmer's Guide and Specification


개발 과정


1. Java에서 native method 선언


먼저 Java에서 native method를 선언한다.

package helloJNI;

public class HelloJNI
{
    static
    {
        System.loadLibrary("hellojni");
    }

    public native String printHello(String message);

    public static void main(String[] args)
    {
        HelloJNI helloJNI = new HelloJNI();
        System.out.println("Hello, from " + helloJNI.printHello("Java") + ".");
    }
}

native method는 "native" 키워드로 선언할 수 있으며, abstract method처럼 내용을 구현하지 않고 세미콜론으로 끝낸다.

native method는 런타임에 C로 작성된 공유 라이브러리의 함수를 호출하여 동작하므로 공유 라이브러리 파일이 필요하다. 이것은 System.loadLibrary() 메소드를 통해 이루어질 수 있으며, 인자로 공유 라이브러리의 이름이 들어간다.
위의 예에서 공유 라이브러리 이름은 "hellojni"이며, 이에 해당하는 공유 라이브러리 파일의 이름은 Windows에서 "hellojni.dll"이고, Linux에서 "libhellojni.so"이다.


2. Header file 생성하기


C로 JNI를 통해 Java에 제공할 함수를 만들기 전에, Java에서 이해할 수 있는 함수 프로토타입이 선언된 헤더 파일을 생성해야 한다.
헤더 파일은 JDK 내에 포함된 실행 파일인 javah를 통해 생성할 수 있다.

eclipse를 사용하여 간단히 javah를 사용하는 방법도 있지만 여기서는 명령행에서 사용하는 방법을 소개한다.

javah는 간단히 다음과 같이 사용할 수 있다.

> javah.exe [패키지명].[클래스명]

만약 이 명령어를 호출하는 디렉토리에 컴파일된 Java 패키지가 없는 경우에는 다음과 같이 경로를 지정해줄 수 있다.

> javah.exe -classpath .;[경로] [패키지명].[클래스명]

경로에는 eclipse로 개발하는 경우 프로젝트의 bin 디렉토리의 경로를 입력하면 된다.

".;"는 .class 파일을 검색할 디렉토리에 현재 디렉토리를 추가한다는 의미로 여기서는 없어도 무방하다. 여러 디렉토리에 대해 검색하고 싶은 경우 여러 경로를 세미콜론으로 구분하면 된다.

그 밖에 다른 유용한 옵션은 여기를 참고하자.

이번 예에서는 다음과 같이 헤더 파일을 생성하였다. (eclipse project의 bin 디렉토리 내에서 실행)

> javah.exe helloJNI.HelloJNI

이렇게 하면 현재 작업 디렉토리에 "helloJNI_HelloJNI.h"이라는 이름의 헤더 파일이 생성된다. [패키지명]_[클래스명].h 형식이며, 패키지 경로의 구분자 '.'은 '_'로 대체된다.

다음은 생성된 헤더 파일이다.

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class helloJNI_HelloJNI */

#ifndef _Included_helloJNI_HelloJNI
#define _Included_helloJNI_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     helloJNI_HelloJNI
 * Method:    printHello
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_helloJNI_HelloJNI_printHello
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

여기에 선언된 "Java_helloJNI_HelloJNI_printHello()" 함수를 구현하면 된다.


3. C언어로 JNI를 통해 Java에 제공할 함수 구현


이렇게 구현했다.

#include <stdio.h>
#include <jni.h>

#include "helloJNI_HelloJNI.h"

JNIEXPORT jstring JNICALL Java_helloJNI_HelloJNI_printHello
    (JNIEnv *env, jobject obj, jstring message)
{
    const char* msg = NULL;
    char* buf = "C world";

    msg = (*env)->GetStringUTFChars(env, message, NULL);
    if(msg == NULL)
        return NULL; /* error occurred */

    printf("Hello, from %s.\n", msg);

    (*env)->ReleaseStringUTFChars(env, message, msg);

    return (*env)->NewStringUTF(env, buf);
}

"jni.h" 파일을 include해야 하며, 이전 순서에서 생성한 헤더 파일도 include해야 한다.

위의 예제는 인자로 받은 String을 출력한 다음 다른 String을 return한다.

C에서 String을 포함한 Java object를 다루는 방법은 다음 글에서 설명한다.

이제 빌드하여 공유 라이브러리 파일을 생성하자


4. 공유 라이브러리 빌드 및 실행


음 그냥 빌드해서 공유 라이브러리를 생성하면 된다. 방법은 다양하다.
플랫폼에 따라 Visual studio를 사용해도 되고 mingw나 gcc를 사용해도 된다.

그러나 Java의 범용성을 활용하기 위해서는 여러 플랫폼에서, 또는 한 플랫폼에서 여러 플랫폼을 타겟으로 빌드하는 환경을 만들어 놓는 것이 좋다.
그래서 여기서도 CMake를 사용할 것이다.

CMake를 사용하면 단 한번의 빌드 형상 정의를 통해 여러 플랫폼에서 빌드할 수도 있고 크로스 컴파일을 통해 다른 플랫폼을 타겟으로 빌드할 수 있다.

즉 이 글에서 정의한 빌드 형상(CMakeLists.txt) 파일 하나로 다양한 타겟의 공유 라이브러리를 쉽게 만들 수 있다.

이 예제에서 CMakeLists.txt 파일은 다음과 같다.

cmake_minimum_required(VERSION 2.8.4)

if(WIN32)
    set(JDK_ROOT "D:/Program/Java/jdk1.8.0_172")
elseif(UNIX)
    set(JDK_ROOT "/usr/lib/jvm/java-8-openjdk-amd64")
endif()

include_directories(${JDK_ROOT}/include)
if(WIN32)
    include_directories(${JDK_ROOT}/include/win32)
elseif(UNIX)
    include_directories(${JDK_ROOT}/include/linux)
endif()

add_library(hellojni SHARED hellojni.c)

"JDK_ROOT" 변수의 값은 각자 환경에 맞게 수정하자.
시스템에 설치된 JDK의 루트 디렉토리로 설정하면 된다.

이 파일을 통해 빌드 형상은 다음과 같의 정의된다.
1. JDK 내에 있는 include 디렉토리와 그 안에 win32 디렉토리를 include directory 경로에 추가한다.
2. 공유 라이브러리로 빌드하기 위해 add_library()에 SHARED 옵션을 사용한다.
3. 이 때 공유 라이브러리 이름은 Java에서 System.loadLibrary() 메소드의 인자로 사용한 문자열과 동일해야 한다.

Visual studio 등을 사용할 때에도 위와 같이 설정하면 된다. 즉 include directory 경로 추가와, 공유 라이브러리 빌드 설정을 하면 된다.

공유 라이브러리가 생성되었으면 이제 실행하면 된다.
단, 실행하기 전에 공유 라이브러리 파일의 경로는 각자 환경에 맞게 잘 설정해야 한다.

Hello, from C world.
Hello, from Java.

2017년 10월 27일 금요일

[Build System] Windows 플랫폼에서 ARM Linux 크로스 컴파일 환경 Standalone으로 구성

IDE 등으로 배포하기 위해 Windows 플랫폼에서 ARM 크로스 컴파일 환경을 Standalone으로 구성하려고 한다.

준비물 : mingw32 arm linux toolchain, cmake, mingw32-make

우선 작업을 위한 폴더를 만든다. 나는 D: 바로 아래에 arm이라는 폴더를 만들었다.
그 폴더 안에 다음 링크를 통해 받은 파일을 저장한다.

mingw32 arm liunux 툴체인 : https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/
이 사이트에서 "gcc-linaro-[버전]-[날짜]-i686-mingw32_arm-linux-gnueabihf.tar.xz" 파일을 다운받는다.
(다른 버전은 여기 참조)

cmake : https://cmake.org/files/v3.10/cmake-3.10.0-rc3-win64-x64.zip
(역시 다른 버전을 원하면 여기 참조)

Standalone으로 구성하기 위해 cmake는 zip 파일로 내려받는다.

위 파일들을 받고 압축을 해제한다.

mingw32-make는 정식 경로에서 Standalone으로 받는 방법은 찾지 못했다.
대신에 다음 사이트에서 mingw-get을 받아 설치한 후 mingw32-make.exe를 가져오는 방법을 사용하였다.
https://sourceforge.net/projects/mingw/

설치 후 mingw-get GUI에서 다음과 같이 mingw32-make를 선택해서 설치하거나


명령 프롬프트에서 설치할 수 있다.

> C:\MinGW\bin\mingw-get.exe install mingw32-make

어떤 방법으로든 설치하고 나면 MinGW 설치 폴더 아래 bin 폴더에 다음과 같은 파일이 생길 것이다.


이 파일들 중 mingw-get.exe를 제외한 5개 파일을 작업 폴더에 mingw32-make라는 폴더를 만들고 그 아래에 복사한다.

준비물을 모두 챙기고 나면 작업 폴더는 다음과 같이 될 것이다.


이제 지난 글에서처럼 툴체인을 명시한 toolchain.arm.cmake 파일을 작성한다.

toolchain.arm.cmake
SET(CMAKE_SYSTEM_NAME Linux)
SET(CMAKE_SYSTEM_PROCESSOR arm)

SET(COMPILER_ROOT "D:/arm/gcc-linaro-7.1.1-2017.08-i686-mingw32_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-")

SET(CMAKE_C_COMPILER ${COMPILER_ROOT}gcc.exe)
SET(CMAKE_CXX_COMPILER ${COMPILER_ROOT}g++.exe)
SET(CMAKE_LINKER ${COMPILER_ROOT}ld.exe)
SET(CMAKE_NM ${COMPILER_ROOT}nm.exe)
SET(CMAKE_OBJCOPY ${COMPILER_ROOT}objcopy.exe)
SET(CMAKE_OBJDUMP ${COMPILER_ROOT}objdump.exe)
SET(CMAKE_RANLIB ${COMPILER_ROOT}ranlib.exe)

경로는 각자 환경에 맞게 수정하면 된다. 단, 절대경로를 사용해야 한다.
파일 구분자를 백슬레시(\)로 입력하면 바보같은 cmake가 자꾸 escape character로 처리하고 못알아먹겠다고 징징거리니 슬레시(/)로 입력하도록 하자.

이제 역시 또 간단한 예제 소스코드를 작성한다.

main.c
#include <stdio.h>

int main()
{
    printf("hello arm!!\n");
    return 0;
}

CMakeLists.txt
ADD_EXECUTABLE(App main.c)

이제 준비는 모두 끝났으니 빌드하면 된다.

매번 명령어를 입력하기는 귀찮으니 배치 파일을 만들자

build.bat
mkdir build
cd build

"../cmake-3.10.0-rc3-win64-x64/bin/cmake.exe" -DCMAKE_MAKE_PROGRAM="D:/arm/mingw32-make/mingw32-make.exe" -DCMAKE_TOOLCHAIN_FILE=../toolchain.arm.cmake -G "MinGW Makefiles" ..

"../cmake-3.10.0-rc3-win64-x64/bin/cmake.exe"  --build .

@echo off
set /p str=completed

준비물들이랑 빌드 결과물이 섞이면 곤란하니 build 폴더를 생성하고 그 아래에 빌드 결과물이 생성되도록 하였다.

세 번째 명령어가 매우 긴데, 다음과 같은 일을 한다.

-DCMAKE_MAKE_PROGRAM : mingw32-make.exe의 경로를 지정한다.(절대경로) 각자 환경에 맞게 수정하자.
mingw-get을 설치하고 나서는 이 옵션이 없어도 cmake가 알아서 mingw32-make의 경로를 찾아내는데, mingw-get 없이 Standalone으로 실행하려면 이 옵션이 반드시 필요하다.

-DCMAKE_TOOLCHAIN_FILE : 툴체인을 명시한 toolchain.arm.cmake 파일의 경로를 지정한다. 역시 각자 환경에 맞게 수정하자.

-G "MinGW Makefiles" : mingw32-make.exe가 이해할 수 있는 Makefile을 생성한다.

build.bat 파일을 실행하고 나면 build 폴더 안에 App이라는 파일이 생성되었을 것이다.

이것을 ARM Linux 플랫폼에 옮기고 실행하면!

2017년 10월 26일 목요일

[Build System] cmake에서 크로스 컴파일러(툴체인) 사용하기 (x86_64 Linux에서 ARM Linux 실행 파일 빌드)

지난 번에는 cmake를 사용하여 서로 다른 플랫폼에서 빌드하는 방법을 알아보았는데, 이번엔 한 플랫폼에서 다른 플랫폼 실행 파일을 크로스 컴파일하는 방법을 알아볼 것이다.

예제로, 데스크탑 PC(x86_64 Ubuntu 16.04)에서 ARM Linux 실행 파일을 크로스 컴파일해 볼 것이다.

먼저 툴체인을 받아와서 압축을 해제한다.

# wget https://releases.linaro.org/components/toolchain/binaries/latest/arm-linux-gnueabihf/gcc-linaro-7.1.1-2017.08-x86_64_arm-linux-gnueabihf.tar.xz
# tar xf gcc-linaro-7.1.1-2017.08-x86_64_arm-linux-gnueabihf.tar.xz

원하는 버전의 다른 ARM 툴체인은 여기서 찾아볼 수 있다.

이제 toolchain.arm.cmake 라는 파일을 만들어서 내용을 다음과 같이 채워 넣는다.

toolchain.arm.cmake
SET(CMAKE_SYSTEM_NAME Linux)
SET(CMAKE_SYSTEM_PROCESSOR arm)

SET(COMPILER_ROOT /root/cmake/gcc-linaro-7.1.1-2017.08-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-)

SET(CMAKE_C_COMPILER ${COMPILER_ROOT}gcc)
SET(CMAKE_CXX_COMPILER ${COMPILER_ROOT}g++)
SET(CMAKE_LINKER ${COMPILER_ROOT}ld)
SET(CMAKE_NM ${COMPILER_ROOT}nm)
SET(CMAKE_OBJCOPY ${COMPILER_ROOT}objcopy)
SET(CMAKE_OBJDUMP ${COMPILER_ROOT}objdump)
SET(CMAKE_RANLIB ${COMPILER_ROOT}ranlib)

이 파일은 툴체인의 경로(절대경로)를 명시하는 역할을 한다. 나중에 cmake를 실행할 때 이 파일을 입력해서 빌드시 사용할 툴체인을 지정해줄 수 있다.
첫 번째 줄의 COMPILER_ROOT 변수는 각자 환경에 맞게 수정하면 된다. 여기서는 앞에서 받아온 ARM 툴체인의 실행파일(gcc, ld 등)들의 경로를 지정하였다.

예제로 빌드할 소스코드(main.c)와 cmake 파일(CMakeLists.txt)를 작성하자.

main.c
#include <stdio.h>

int main()
{
    printf("hello arm!\n");
    return 0;
}

CMakeLists.txt
ADD_EXECUTABLE(App main.c)

이제 다음 명령어를 입력하면 빌드된다!

# cmake -DCMAKE_TOOLCHAIN_FILE=toolchain.arm.cmake .
# make

App 이라는 실행파일이 생성되었을 것이다. file 명령어를 통해 확인해 보면 잘 컴파일 된 것을 확인할 수 있다.


이 파일을 ARM 플랫폼으로 (여기서는 raspberry pi에) 전송한 후에 실행한 화면이다.

2017년 7월 31일 월요일

[Build System] cmake를 활용하여 windows와 linux 모두에서 빌드 가능한 프로젝트 만들기

  이번 글에서는 제목과 같이 cmake 툴을 활용해서 Windows와 Linux 모두에서 빌드할 수 있는 프로젝트를 만들어 볼 것이다. 우선 cmake는 소스코드들과 그 결과물인 바이너리, 라이브러리의 구조를 추상화해서 빌드 플랫폼에 의존적인 Makefile(이나 등등)을 생성해내는 도구이다. 자세한건 여기를 참고하자.

  먼저 cmake를 설치한다. https://cmake.org/download/ 이 사이트에서 플랫폼에 맞는 바이너리를 받을 수도 있고 아니면 http://lablk.blogspot.kr/2017/07/ethercat-soem-ethercat.html에서처럼 소스코드를 받아서 직접 빌드할 수도 있다. 다행히 이 소스코드를 빌드할 때에는 cmake를 사용하지 않는다.

# apt-get install cmake

이렇게 해도 된다.

  예제를 기준으로 Windows와 Linux 각각 플랫폼에 대해 다른 빌드 구조나 소스코드를 선택하는 법과, 실제 빌드를 어떻게 수행하는지 알아볼 것이다.

  먼저 cmake의 빌드 구조를 정의한 CMakeLists.txt 파일에서는 현재 빌드 플랫폼이 무엇인지 다음과 같이 확인할 수 있다.

if(WIN32)
    message("win 32 build!")
elseif(UNIX)
    message("linux build!")
endif()

if문과 WIN32, UNIX 키워드를 통해서 확인이 가능하다. Windows 64 타겟으로 빌드할 때에도 WIN32 키워드가 활성화된다. 이 분기문을 통해 각 빌드 플랫폼에 따라 서로 다른 소스코드나 라이브러리를 선택하는 등의 빌드 구조를 정의할 수 있다.

  타겟 플랫폼에 따라 소스코드 파일을 따로 작성할 수도 있지만 그 양이 많지 않다면 한 소스파일 내에서 전처리기 매크로로 구분하는 방법도 좋다.

#if defined(_WIN64)
    printf("_WIN64!!\n");
#elif defined(_WIN32)
    printf("_WIN32!!\n");
#elif defined(__linux__)
    printf("__linux__");
    if(sizeof(long int) == 8)
        printf("64!!\n");
    else
        printf("32!!\n");
#endif

이런 식으로 하면 되는데.. Linux 플랫폼에서 64비트, 32비트로 동작하는지 알아보는 매크로가 없는지 잘 몰라서 일단 sizeof(long int)로 파악해 보도록 했다. 더 좋은 방법 아는 사람은 알려주기 바람..

  마지막으로 각 플랫폼에서의 빌드 방법이다. 먼저 Linux에서는

$ mkdir build
$ cd build
$ cmake ..
$ make

이렇게 하면 된다. build 디렉토리 만든거는 그냥 빌드 결과물이 소스코드와 섞이지 않도록 하기 위한 거다.

  Windows에서는 조금 더 복잡하다. 우선 Visual Studio를 설치해야 한다. cmd.exe나 Visual Studio 명령 프롬프트를 열어서 다음과 같이 명령어를 입력한다. (다른 데서는 Visual Studio 명령 프롬프트만 사용해야 한다고 한 것 같은데 cmd에서도 동작하는 것 같다. 착각이면 알려주기 바람.)

> mkdir build
> cd build
> "C:\Program Files\CMake\bin\cmake.exe" .. -G "Visual Studio 15 2017 Win64"
> "C:\Program Files\CMake\bin\cmake.exe" --build . --config Release

cmake.exe의 경로는 자신의 환경에 맞게 바꾸면 되고, 자신이 설치한 Visual Studio 버전에 따라 3번째 명령어를 다르게 하면 된다. cmake.exe -G 라고만 치면 큰 따옴표 사이에 넣을 수 있는 내용이 쭉 나온다. 여기서 알맞은 것을 선택하자.


엄청 많이 나온다.. Makefile뿐만 아니라 각 IDE의 프로젝트를 그냥 생성해준다.
Visual Studio에 있는 [arch] 부분에는 32비트 타겟으로 빌드할 경우 생략, 64비트 타겟으로 빌드할 경우 "WIN64"라고 입력해주면 된다.