레이블이 l-etc인 게시물을 표시합니다. 모든 게시물 표시
레이블이 l-etc인 게시물을 표시합니다. 모든 게시물 표시

2018년 7월 4일 수요일

[JNI] 객체 사용과 메모리 관리

개요


JNI를 통해 C에서 Java의 객체에 접근할 수 있고 자유롭게 생성할 수 있다. 그러나 C에서 접근했거나 생성한 객체가 여전히 C에서 참조를 갖고 있는지 알 수 없으므로, garbage collector가 이러한 객체를 어떻게 처리할지 알 수 없게 된다. 이를 위해 JNI는 C에서 해당 객체에 대한 참조를 명시적으로 제거할 수 있는 함수를 제공하여 garbage collector가 잘 동작할 수 있도록 메커니즘이 마련되어 있다.

이번 글에서는 C에서 Java 객체를 사용하는 방법과, 메모리 관리 방법을 소개한다.


참고 자료


The Java Native Interface: Programmer's Guide and Specification


1. String


C에서 String을 처리할 수 있는 함수는 다음과 같다.


다음과 같이 사용할 수 있다.

Hello.java
public class Hello
{
    public native String getText(String message);
}

hello.c
JNIEXPORT jstring JNICALL
Java_Hello_getText(JNIEnv *env, jobject obj, jstring message)
{
    char buff[255];
    const char *msg;

    /* get string from java String object */
    msg = (*env)->GetStringUTFChars(env, message, NULL);
    if(msg == NULL)
        return NULL; /* OutOfMemoryError already thrown */

    printf("received from java : %s\n", msg);

    /* free the memory allocated for msg */
    (*env)->ReleaseStringUTFChars(env, message, msg);

    scanf("%s", buff);

    /* create java String object */
    return (*env)->NewStringUTF(env, buff);
}

Java String은 GetStringUTFChars() 함수를 통해 C 문자열(캐릭터의 배열)로 가져올 수 있다.
가져온 문자열은 사용이 모두 끝난 후에 ReleaseStringUTFChars() 함수를 통해 할당된 메모리 영역을 반환해야 한다.

C 문자열로부터 NewStringUTF() 함수를 통해 java String 객체를 생성할 수 있다.
이를 통해 생성된 객체는 전적으로 java에서만 사용되는 것으로 간주되며, C에서 참조를 갖고 있더라도 garbage collector가 이를 확인하지 않으므로, java에서의 참조만 없다면 해당 객체는 제거될 수 있다.


2. Object construction


객체를 생성하는 순서는 다음과 같다.
1. 생성할 객체의 class를 얻는다.
2. 생성자(constructor)를 얻는다.
3. 생성자 매개변수와 함께 객체를 생성한다.

다음은 사용 예이다.

JNIEXPORT jobject JNICALL
Java_Hello_getObject(JNIEnv *env, jobject obj)
{
    jClass class;
    jmethodID constructor;
    int parameter = 1;
    jobject result;

    /* get class */
    class = (*env)->FindClass(env, "java/lang/Integer");
    if(class == NULL)
        return NULL;

    /* get constructor */
    constructor = (*env)->GetMethodID(env, class, "<init>", "(I)V");
    if(constructor == NULL)
        return NULL;

    /* construct object */
    result = (*env)->NewObject(env, class, constructor, parameter);

    return result;
}

FindClass() 함수를 통해 특정 클래스를 얻어올 수 있다. 두 번째 매개변수로 얻어올 클래스의 패키지 경로를 포함한 전체 이름을 적는다.

GetMethodID() 함수는 특정 클래스의 메소드를 얻어오는 함수이다. 원래 세 번째 매개변수에는 메소드의 이름, 네 번째 매개변수에는 메소드의 시그니처를 넣어야 하지만, 생성자를 얻어올 경우 메소드의 이름을 "init"으로, 시그니처의 반환 타입은 void를 의미하는 "V"로 고정해야 한다.

메소드 시그니처는 메소드의 반환 타입과 매개변수들의 타입을 문자열로 정의한 것으로 "({매개변수}){반환타입}" 형식이다.

각각 java 타입에 해당하는 시그니처는 다음과 같다.

TypeSignature
voidV
booleanZ
byteB
charC
intI
longJ
floatF
doubleD
objectL{패키지 경로를 포함한 클래스 전체 이름};
type[][{해당 타입의 시그니처}

예를 들어서

void aaa() -> ()V
int bbb(boolean a, char, b) -> (ZC)I
String[] ccc(int[] c, MyClass d) -> ([ILmyPackage/MyClass;)[Ljava/lang/String;

이 된다.

마지막으로 NewObject() 함수를 통해 해당 객체를 생성한다. 4번째 파라미터부터는 지정된 생성자의 파라미터로 사용될 변수를 순서대로 넣으면 된다. 만약 생성자의 파라미터가 두 개라면 4번째 파라미터에 생성자의 1번째 파라미터, 5번째 파라미터에 생성자의 2번째 파라미터를 넣으면 된다.


3. Array


다음은 배열을 처리하는 함수들이다.


<Type>에 원하는 배열 타입을 입력하면 된다. 예를 들어 int 배열을 생성하고 싶은 경우
NewIntArray() 함수를 사용한다.

Get<Type>ArrayRegion() 함수와 Get<Type>ArrayElements() 함수는 모두 배열의 값을 얻어올 수 있다는 공통점이 있지만 사용 방법이 다르다.

Get<Type>ArrayRegion() 함수는 다음과 같이 얻어올 메모리 영역이 미리 확보되어 있을 때 사용한다.

JNIEXPORT jint JNICALL
Java_Hello_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
    int buf[10];
    int i, sum = 0;

    /* get int array data */
    (*env)->GetIntArrayRegion(env, arr, 0, 10, buf);

    for(i = 0; i < 10; i++)
        sum += buf[i];

    return sum;
}

반면 Get<Type>ArrayElements() 함수는 메모리 영역이 확보된 배열 포인터를 반환한다. 따라서 해당 포인터를 모두 사용하고 난 다음에는 Release<Type>ArrayElements() 함수를 통해 해당 메모리 영역을 반환해야 한다.

JNIEXPORT jint JNICALL
Java_Hello_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
    int *buf;
    int i, sum = 0;

    /* get int array data */
    buf = (*env)->GetIntArrayElements(env, arr, NULL);
    if(buf == NULL)
        return 0;

    for(i = 0; i < 10; i++)
        sum += buf[i];

    /* release int array memory */
    (*env)->ReleaseIntArrayElements(env, arr, buf, 0);

    return sum;
}


4. DeleteLocalRef()


만약 C에서 객체를 생성하였는데, Java로 반환되지도 않고 더 이상 사용되지 않는다면 DeleteLocalRef() 함수를 통해 반드시 해당 객체에 대한 참조를 지워야 한다.

예를 들어 다음과 같이 object array를 만드는 경우에 배열의 각 요소는 배열에 넣은 후 반환되지 않고 더 이상 사용되지 않는다. 이 때 DeleteLocalRef() 함수를 호출해야 한다.

JNIEXPORT jobjectArray JNICALL
Java_Hello_getNameList(JNIEnv *env, jobject obj)
{
    int i;
    char buf[255];
    jstring name;

    jclass stringClass;
    jobjectArray nameList;

    /* construct String array */
    stringClass = (*env)->FindClass(env, "java/lang/String");
    if(stringClass == NULL)
        return NULL;

    nameList = (*env)->NewObjectArray(env, 10, stringClass, NULL);
    if(nameList == NULL)
        return NULL;

    printf("enter 10 names\n");
    for(i = 0; i < 10; i++)
    {
        scanf("%s", buf);

        /* construct new String */
        name = (*env)->NewStringUTF(env, buf);
        if(name == NULL)
            return NULL;

        /* insert String to array */
        (*env)->SetObjectArrayElement(env, nameList, i, (jobject)name);

        /* delete local reference */
        (*env)->DeleteLocalRef(env, name);
    }

    return nameList;
}

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.

2018년 5월 3일 목요일

Font display programming for embedded devices (pixel-based)

디스플레이에 문자열을 픽셀 단위로 처리하여 출력하는 알고리즘을 개발하기 위해 먼저 각 문자별로 비트맵이 정의된 C 배열이 필요하다.

C 배열을 구하기 위해 열심히 구글링을 하던 중 굉장한 것을 찾았다.

https://www.mikroe.com/glcd-font-creator

GLCD Font Creator라는 프로그램인데, 시스템에 정의된 폰트와 스타일, 크기를 설정하면 그에 맞는 C 배열을 생성해 준다..

사용법은 쉬우니까 패스.. 하고 생성된 C 배열의 구조는 다음과 같다.


C 배열에서 각각의 줄은 줄 끝에 주석으로 설명된 문자에 해당하는 비트맵이고 순서는 아스키 코드를 따른다.

각각의 줄에서 가장 첫 번째 바이트는 개별 비트맵의 가로 픽셀 길이를 의미한다. 모든 문자의 비트맵은 가로, 세로가 고정된 고정길이로 정의되는데 (그림 예시에서는 7x15 픽셀), 가변길이로 출력하고 싶은 경우 첫 번째 바이트를 보고 비트맵의 특정 부분만 추려서 출력하면 된다.

나머지는 비트맵인데, 왼쪽 위부터 세로로 8비트씩 한 바이트이고 바이트 index는 수직 방향이 우선이다. 한 바이트의 8개 픽셀에서 가장 위쪽 픽셀이 LSB이고 아래쪽 픽셀이 MSB이다.

예를 들어 'a' 문자의 경우 첫 번째 바이트인 0x06은 비트맵 중 폰트가 정의된 가로 픽셀 수를 의미하고 나머지는 다음과 같이 비트맵 데이터를 담고 있다.

따라서 byte 3의 경우 0x06이고 이 값이 배열의 5번째 바이트(index=4)에 존재하는 것을 알 수 있다.

비트맵이 정의된 C 배열을 얻었으니 이제 이 배열을 해석하여 특정 픽셀에 값을 써 주는 알고리즘을 개발하면 된다. 다음은 그 코드이다.

#define LCD_WIDTH 256
#define LCD_HEIGHT 64

int draw_string(const char* string, int base_x, int base_y,
    const unsigned char* font, int width, int height, int start, int end,
    int spacing, int line_spacing, int monospace)
{
    int x, y;
    int cur_x, cur_y, char_x, char_y;

    int height_byte = (height + 7) / 8;
    int entry_size = height_byte * width + 1;

    int index;
    int bitmap_width;
    const unsigned char* bitmap = NULL;

    unsigned char data;
    unsigned char bit_index;
    unsigned char bit;

    char_x = base_x;
    char_y = base_y;
    cur_x = char_x;
    cur_y = char_y;

    if((cur_y + height) > LCD_HEIGHT)
        return -1; /* out of LCD size */
    if(width <= -spacing)
        return -1; /* invalid argument */

    while(*string != '\0')
    {
        /* new line character */
        if(*string == '\n')
        {
            char_x = base_x;
            cur_x = char_x;
            char_y += height + line_spacing;
            cur_y = char_y;
            string++;
            continue;
        }

        if(*string < start || *string > end)
        {
            string++;
            continue; /* invalid character */
        }

        /* get character data */
        index = *string - start;
        if(monospace)
            bitmap_width = width;
        else
            bitmap_width = font[entry_size * index];
        bitmap = &(font[entry_size * index + 1]);

        if(char_x + bitmap_width > LCD_WIDTH)
            return -1; /* out of LCD size */

        /* draw character */
        for(x = 0; x < bitmap_width; x++)
        {
            for(y = 0; y < height_byte; y++)
            {
                data = bitmap[x * height_byte + y];

                for(bit_index = 0; bit_index < 8; bit_index++)
                {
                    if((y * 8 + bit_index) >= height)
                        break; /* skip unused bits */

                    bit = data & 0x01;
                    data >>= 1;

                    /*
                     * TODO: Implement draw_pixel() function
                     * for your target system.
                     */
                    draw_pixel(cur_x, cur_y, bit);
                    cur_y++;
                }
            }
            cur_x++;
            cur_y = char_y;
        }

        /* process the next character */
        char_x += bitmap_width + spacing;
        cur_x = char_x;
        string++;
    }

    return 0;
}

이 코드를 사용하기 위해서는 다음을 수정해야 한다.
1) LCD_WIDTH와 LCD_HEIGHT를 타겟 디스플레이의 가로/세로 픽셀 길이로 정의
2) draw_pixel() 함수를 각자 타겟 디스플레이에 맞는 코드로 구현

int draw_pixel(x, y, bit) 함수는 디스플레이의 왼쪽 위를 (0, 0) 좌표로 정의했을 때 (x, y) 위치에 해당하는 픽셀을 bit 값으로 설정하는 함수이다. bit는 0 또는 1이 입력된다.

이 코드의 파라미터에 대한 설명은 다음과 같다.
1) string: 출력할 문자열 ('\0' 문자로 끝나는)
2) base_x: 문자열의 왼쪽 위 점이 위치할 디스플레이 상의 x 좌표
3) base_y: 문자열의 왼쪽 위 점이 위치할 디스플레이 상의 y 좌표
4) font: 앞에서 GLCD font creator를 통해 생성한 C 배열
5) width: 폰트의 가로 픽셀 길이
6) height: 폰트의 세로 픽셀 길이
7) start: 폰트에 정의된 시작 문자의 아스키 코드
8) end: 폰트에 정의된 끝 문자의 아스키 코드
9) spacing: 자간
10) line_spacing: 줄 간격
11) monospace: 1이면 고정길이 출력 0이면 가변길이 출력

파라미터가 많은데, 기존 코드에서는 구조체를 통해 함수를 호출하도록 하였으나 설명을 위해 하나하나 풀어서 작성하였다.

width, height는 폰트의 가로 및 세로 픽셀 수이다. GLCD font creator에서 생성하는 C 배열은 가변길이 폰트라도 크기가 가장 큰 문자를 기준으로 가로 및 세로 픽셀 수가 모든 문자에 대해 고정으로 정의된다. (빈 부분의 픽셀 값은 0)

start, end의 경우 GLCD font creator에서 C 배열을 생성할 때 메모리 절약을 위해 사용하지 않는 아스키 코드 구간을 삭제하고 비트맵을 생성하도록 설정할 수 있는데, 여기서 생성하도록 설정된 아스키 코드 구간의 첫 문자가 start이고, 마지막 문자가 end이다.
기본 설정의 경우 32 ~ 127 구간의 C 배열을 생성하므로 start=32, end=127을 지정하면 된다.

spacing, line_spacing: 무려 자간과 줄간격도 설정할 수 있도록 구현하였다.
monospace: 가변길이 문자열을 고정길이로 출력할 경우 어색하기 때문에 monospace 파라미터를 사용하여 어떤 방식으로 출력할지 설정하도록 하였다.

위 함수의 사용 예는 다음과 같다.

draw_string("Hello\nWorld!", 0, 0, consolas7x15, 7, 15, 32, 127, 0, 0, 1);

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년 10월 17일 화요일

[Machine Learning] mldivide ('\')를 활용한 linear regression

1. 정의


행렬 왼쪽 나눗셈 (mldivide, '\')는 행렬 A, X, Y로 이루어진 다음 시스템에 대해
A * X = Y
다음과 같이 정의된다.
A \ Y = X 
A * (A \ Y) = Y

A가 역행렬이 존재하는 정사각 행렬일 경우 다음과 같이 계산할 수 있다.
A \ Y = inv(A) * Y = X

그러나 mldivide는 A의 역행렬이 존재하지 않거나 심지어 정사각 행렬이 아닐 경우에도 정의되는데, 이를 계산하기 위해 다음 링크에 있는 MATLAB 문서는 다음과 같이 그 알고리즘을 설명하고 있다.
https://kr.mathworks.com/help/matlab/ref/mldivide.html#bt4jslc-6
복잡한 알고리즘은 모두 A가 정사각 행렬인 경우에 대한 내용이고, 정사각 행렬이 아닌 경우 QR solver로 해결하도록 되어 있다. MATLAB에는 mldivide 연산에 위와 같은 알고리즘이 구현되어 있어서, 이를 통해 정사각 행렬이 아닌 경우에도 연산이 가능하며, 나아가 linear regression도 mldivide로 계산할 수 있다.


2. 의미



위 그림과 같이 행렬 A의 크기를 n * m, Y의 크기를 n * l로 정의할 경우 n, m, l은 각각 다음을 의미한다.

  • n : 시스템을 이루는 식의 수
  • m : 시스템을 이루는 미지수의 수 (차원)
  • l : 시스템의 개수 (각 시스템은 동일한 계수를 갖지만, 미지수와 상수는 다름)

예를 들어 다음과 같은 시스템에 대해


행렬로 다음과 같이 표현할 수 있고, mldivide로 해를 구할 수 있다.


n과 m이 동일하면서 A의 rank가 m과 같은 경우, 즉 역행렬이 존재하는 경우는 위와 같이 풀이가 가능하다.

그러나 정사각 행렬이 아닌 경우 n과 m의 관계에 따라 mldivide는 다음과 같은 의미를 갖는다.

  • n(또는 A의 rank)이 m보다 작은 경우 : 무수히 많은 해
  • n이 m보다 큰 경우 : Least square를 만족하는 Linear regression

n이 m보다 작은 경우 mldivide는 다음과 같이 m - n(또는 A의 rank)개에 해당하는 미지수를 0으로 설정한 후 나머지 미지수에 대한 값을 계산한다.



3. Linear Regression


n이 m보다 큰 경우 선형 시스템 상에서는 해가 존재하지 않게 된다. 대신에 mldivide는 Least square를 만족하는 Linear regression으로 동작한다. 따라서 다음 식과 같이 행렬곱이 mldivide의 역연산이 될 수 없다.
A * (A \ Y) ≠ Y
Linear regression의 경우 n, m의 의미는 다음과 같이 재 정의될 수 있다. l은 동일한 데이터, 속성에 대한 단순 반복이므로 중요하지 않다.

  • n : 데이터의 수
  • m : 속성의 수

이에 따라 행렬 A는 기존의 계수 행렬(Coefficient Matrix)에서 각 속성들의 관계를 나타내는 데이터들의 집합, 즉 Training data의 집합으로 재해석할 수 있고, Y 행렬도 동일하게 Training data 집합의 속성을 갖는다. Linear regression의 결과인 X 행렬은 각 속성들의 가중치를 나타낸다.

가장 간단하게, n개의 두 속성 x, y를 갖는 데이터를 이용하여 다음과 같은 선형 모델로 Regression할 때


행렬 A, Y와 X는 다음과 같이 정의된다.


행렬 A는 상수항을 의미하는 1로 이루어진 열과 각각의 속성에 해당하는 데이터로 이루어진 열들로 구성되며, Y는 남은 한 속성에 해당하는 데이터로 구성된다.
행렬 X는 상수항과 속성의 계수로 구성되며, 선형 모델을 나타낸다.

예를 들어서 키와 몸무게 사이의 관계에 관한 Linear regression 문제를 MATLAB을 사용하여 푸는 방법은 다음과 같다.


최종적으로 다음과 같은 선형 모델을 얻게 된다.



4. 계산


n > m인 행렬 A에 대해 일반적인 경우에는 다음과 같이 계산이 가능하다.
X = A \ Y = inv(A' * A) * A' * Y            (A'는 A의 Transpose)
그러나 다음 행렬과 같이 데이터의 스케일 차이가 큰 경우 부동소수점 연산 정밀도의 한계로, 정확한 해를 구할 수 없게 된다.


MATLAB에는 해를 구하기 위해 다음의 여러 가지 방법들을 제공한다.


  • QR Solver :
        [Q, R] = qr(A, 0)
        X = R \ (Q' * Y)
  • X = pinv(A) * Y
  • X = linsolve(A, Y)


앞의 예제를 각각의 방법으로 구한 해와 RMSE는 다음과 같다.

exact
solution
A \ YQR Solverpinv(A) * Ylinsolve(A, Y)inv(A' * A) * A' * Y
b0.0-0.0127-0.0555-0.0313-0.01270.0041
a1.01.00001.00001.00001.00001.0000
RMSE0.00.00780.03490.05900.00780.1243

A \ Y와 linsolve의 성능이 가장 좋은 것을 확인할 수 있다.
반면 상수항에 비해 데이터가 작은 경우에는 QR Solver가 가장 정확했다.
exact
solution
A \ YQR Solverpinv(A) * Ylinsolve(A, Y)inv(A' * A) * A' * Y
b0.0-1.05998e-28-5.5511e-29-8.2740e-29-1.05998e-28-1.4724e-28
a1.01.00001.00001.00001.00001.0000
RMSE0.05.6798e-293.1554e-294.0409e-295.6798e-296.1834e-29

경우에 따라 다른 방법의 성능이 더 좋을 수 있으며, 데이터의 특성에 따라 적절한 방법을 선택해야 한다.

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"라고 입력해주면 된다.