2021년 12월 12일 일요일

[Unity] Unity 2019.3 이상에서 Bridge Header 없이 Swift로 iOS 플러그인 개발하기

개요

  • Unity에서는 iOS 네이티브와의 인터페이스를 Objective-C로만 제공한다. 따라서 Swift로 플러그인을 제작하려면 Objective-C가 Unity와 Swift를 연결해 주어야 한다. Unity 2019.2 버전까지는 Bridge Header를 활용해서 Objective-C와 Swift를 연동할 수 있었다.
  • 그러나 Unity 2019.3 버전에서 Unity as a Library가 도입되면서 플러그인이 Xcode 상에서 기존의 Unity-iPhone 타겟이 아닌 UnityFramework 타겟에 포함되는 것으로 바뀌었다.
  • 문제는 Bridge Header를 Framework 타겟에서 사용할 수 없다는 것이다. 따라서 Swift로 플러그인을 제작하려면 Bridge Header 외의 방법을 활용해야 한다.


참고 자료



개발환경



예제 프로그램

  • UI 버튼이 눌리면 유니티는 네이티브 함수를 호출하여 Swift 함수를 실행한다.
  • Swift 함수는 호출될 때마다 숫자를 1씩 더한 후 해당 숫자를 포함한 문자열을 유니티에 전달한다.
  • 유니티에서는 해당 문자열을 받아서 UI 텍스트를 업데이트한다.


1. Unity에서 네이티브 인터페이스 스크립트 작성

NativeInterface.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;

public class NativeInterface : MonoBehaviour
{
    public static event System.Action<string> OnNativeCall;

#if !UNITY_EDITOR && UNITY_IOS
    [DllImport("__Internal")]
    private static extern string CallPluginIOS();
#endif

    public static string CallPlugin()
    {
#if UNITY_EDITOR
        return "";
#elif UNITY_IOS
        return CallPluginIOS();
#else
        return "";
#endif
    }

    public void CallUnity(string message)
    {
        OnNativeCall?.Invoke(message);
    }
}

유니티 -> 네이티브 호출

  • [DllImport("__Internal")]를 사용해서 호출할 네이티브 함수를 선언한다. 이 때 함수명은 나중에 Objective-C에 선언할 함수명과 동일해야 한다.
  • 플랫폼에 독립적인 함수 CallPlugin()를 구현한다. Define symbol로 플랫폼을 구분한다. 여기서는 iOS 플랫폼에서 네이티브 함수인 CallPluginIOS()를 호출한다.
  • 여러 플랫폼을 지원하는 소프트웨어를 개발할 때 이 함수와 같이 플랫폼 의존적인 부분을 추상화하는 함수를 만들어 사용하는 것이 좋다.

네이티브 -> 유니티 호출

  • 마지막으로 네이티브에서 호출될 함수로 CallUnity()를 구현한다. 네이티브에서 호출되는 함수이므로, OnNativeCall라는 이벤트를 통해 다른 스크립트에 제공하도록 한다.
  • 네이티브에서 유니티의 함수를 호출하려면 UnitySendMessage()를 사용하는데, 여기에 게임오브젝트 이름과 함수명이 필요하다. 따라서 이  스크립트를 포함하는 게임오브젝트와 함수명을 기억해 두었다가 Objective-C에 작성해야 한다.


2. 네이티브 플러그인 작성 - Objective-C

objc_bridge.h
1
2
3
4
5
@interface ObjcBridge : NSObject

+ (void) sendMessage: (NSString*)message;

@end

objc_bridge.mm
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#import "objc_bridge.h"
#import "UnityFramework/UnityFramework-Swift.h"

extern "C"
{
    const char* CallPluginIOS()
    {
        NSString *ret = [SwiftPlugin callPlugin];
        const char *nsStringUtf8 = [ret UTF8String];
        char* cString = (char*)malloc(strlen(nsStringUtf8) + 1);
        strcpy(cString, nsStringUtf8);
        return cString;
    }

    void unitySendMessage(const char* message)
    {
        UnitySendMessage("NativeInterface", "CallUnity", message);
    }
}

@implementation ObjcBridge
    
+ (void) sendMessage: (NSString*)message
{
    unitySendMessage([message UTF8String]);
}

@end

유니티 -> Objective-C -> Swift

  • 유니티와의 인터페이스 역할을 할 함수들은 extern "C" 안에 선언한다.
  • 유니티에서 호출될 함수는 함수명, 리턴타입, 파라미터를 맞춰서 선언한다. 여기서는 유니티에서 string CallPluginIOS();로 선언하였으므로 리턴 타입을 string에 대응하는 const char*로 한다.
  • 다음으로 Swift 함수를 호출한다. Swift에 선언된 심볼들을 참조하기 위해서는 UnityFramework/UnityFramework-Swift.himport해야 한다.
  • Swift 함수에서 리턴된 NSStringconst char* 로 바꾸기 위해 UTF8String 함수를 사용한다.
  • 이 문자열을 그대로 리턴하면 제대로 동작하지 않으므로 새로운 메모리를 할당한 후 거기에 문자열을 복사하여 리턴한다.

Swift -> Objective-C -> 유니티

  • Swift에서 호출할 sendMessage() 함수를 헤더파일과 mm 파일에 선언한다.
  • 이 함수는 extern "C" 안에 선언된 unitySendMessage()를 호출한다.
  • unitySendMessage()UnitySendMessage()를 사용하여 유니티의 함수를 호출한다.
    • 이 때 첫 번째 인자로 호출할 함수가 선언된 스크립트가 컴포넌트로 들어있는 게임오브젝트 이름을 전달한다.
    • 두 번째 인자는 호출할 함수 이름을 전달한다.
    • 마지막으로 유니티에 전달하고 싶은 문자열을 전달한다. 이 문자열을 전달하기 위해 sendMessage(), unitySendMessage() 함수는 각각 NSString*const char*를 파라미터로 받는다.


3. 네이티브 플러그인 작성 - Swift

SwiftPlugin.swift
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@objc
public class SwiftPlugin: NSObject
{
    static var count: Int = 0;
    
    @objc
    public static func callPlugin() -> String
    {
        count += 1;
        ObjcBridge.sendMessage("count is " + String(count));
        return "Hello, I'm swift.";
    }
}

Objective-C -> Swift

  • Objective-C에서 참조할 심볼들은 모두 @objcpublic으로 해야 한다.

Swift -> Objective-C

  • Swift에서 Objective-C를 참조하려면 Bridge Header를 쓰지 못하기 때문에 약간 복잡하다. 여기서는 Bridge Header 대신 Umbrella header를  사용할 것이다.
  • Xcode에서 /UnityFramework/UnityFramework.h 파일을 열고 가장 아랫줄에 앞서 작성했던 objc_bridge.h 헤더파일을 import한다.
  • 다음으로 UnityFramework 타겟의 Build Phases 섹션에서 Headers 카테고리의 Public에 objc_bridge.h 헤더파일을 추가한다.
  • 그 다음에는 ObjcBridge 클래스의 sendMessage() 함수를 호출하여 최종적으로 유니티의 함수를 호출한다.


4. 테스트용 스크립트 작성 및 테스트

SampleScript.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class SampleScript : MonoBehaviour
{
    [SerializeField] private Button _button;
    [SerializeField] private Text _test;

    private void Awake()
    {
        _button.onClick.AddListener(delegate
        {
            string ret = NativeInterface.CallPlugin();
            Debug.Log("Call plugin returns: " + ret);
        });

        NativeInterface.OnNativeCall += message =>
        {
            _test.text = message;
        };
    }
}
  • UI 버튼의 onClick 이벤트가 발생하면 네이티브 함수 CallPlugin() 함수를 호출한 후, 그 함수의 리턴값을 로깅한다.
  • 네이티브에서 UnitySendMessage()를 사용하여 NativeInterface.CallUnity() 함수가 호출될 때의 이벤트(NativeInterface.OnNativeCall)로 메시지를 받으면 UI 텍스트에서 해당 메시지를 출력한다.

테스트 영상

  • 로그를 통해 Swift에서 리턴한 메시지도 잘 출력되는 것을 확인할 수 있다.


5. 빌드 스크립트

  • 앞서 진행했던 Umbrella header 관련 작업을 아래와 같은 빌드 스크립트를 통해 자동화할 수 있다.

BuildScript.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
#if UNITY_IOS
using UnityEditor.iOS.Xcode;
#endif

public static class BuildScript
{
#if UNITY_IOS
    private static string[] publicHeaderPaths = new string[]
    {
        "Libraries/Plugins/iOS/objc_bridge.h",
    };

    [PostProcessBuild]
    public static void OnPostProcessBuild(BuildTarget buildTarget, string buildPath)
    {
        string projPath = buildPath + "/Unity-iPhone.xcodeproj/project.pbxproj";
        PBXProject proj = new PBXProject();
        proj.ReadFromFile(projPath);

        string frameworkTarget = proj.GetUnityFrameworkTargetGuid();

        string unityFrameworkHeaderText = File.ReadAllText(buildPath + "/UnityFramework/UnityFramework.h");
        foreach (string headerPath in publicHeaderPaths)
        {
            string headerGuid = proj.FindFileGuidByProjectPath(headerPath);
            proj.AddPublicHeaderToBuild(frameworkTarget, headerGuid);

            string importStatement = "#import \"" + Path.GetFileName(headerPath) + "\"";
            if (!unityFrameworkHeaderText.Contains(importStatement))
                unityFrameworkHeaderText += "\n" + importStatement + "\n";
        }
        File.WriteAllText(buildPath + "/UnityFramework/UnityFramework.h", unityFrameworkHeaderText);

        proj.WriteToFile(projPath);
    }
#endif
}

  • 22-26 line: Xcode 프로젝트 파일을 읽어온다.
  • 28 line: /UnityFramework/UnityFramework.h 파일을 열어서 텍스트를 읽어온다.
  • 31-32 line: 헤더파일을 Build Phases에 public으로 등록한다.
  • 34-36 line: /UnityFramework/UnityFramework.h 파일에 헤더파일을 import한다.
  • 38-40 line: 프로젝트 파일에 데이터를 쓴다.

활용법

  • 이제 빌드 시마다 추가로 해야 하는 작업은 없다.
  • Swift에서 참조할 헤더파일이 추가될 경우 publicHeaderPaths 배열에 헤더파일의 경로를 추가하기만 하면 된다.

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);

2018년 2월 14일 수요일

[EtherCAT] IgH EtherCAT Master Stack API 분석 예제

[준비물]


- IgH EtherCAT Master Stack 1.5.2와 Xenomai OS가 설치된 제어기
- 위 플랫폼에 대한 개발환경 (툴체인, 라이브러리 등)
- Digital I/O EtherCAT Slave 장치


[참고자료]



[예제코드]


#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

#include <native/task.h>
#include <native/timer.h>
#include <ecrt.h>

#define INTERVAL 1000000

static void rt_task_proc(void *arg);
static void sigint_handler(int sig);

/* PDO list to use in application */
unsigned int slave0_6000_01;
unsigned int slave0_6000_01_bit;
unsigned int slave0_7010_01;
unsigned int slave0_7010_01_bit;

static ec_pdo_entry_reg_t pdo_entry_reg[] = {
    {0, 0, 0x0, 0x0, 0x6000, 1, &slave0_6000_01, &slave0_6000_01_bit},
    {0, 0, 0x0, 0x0, 0x7010, 1, &slave0_7010_01, &slave0_7010_01_bit},
    {}
};

static ec_master_t* master = NULL;
static ec_domain_t* domain = NULL;
static uint8_t* domain_pd = NULL;

static int alive = 1;

int main(int argc, char** argv)
{
    int i;
    int ret = 0;

    ec_master_info_t master_info;
    ec_slave_info_t* slave_info_list = NULL;
    ec_slave_config_t* slave = NULL;
    int slave_count = 0;

    RT_TASK* rt_task_plc = NULL;

    /* signal handler registration */
    signal(SIGINT, sigint_handler);

    /* configure master */
    master = ecrt_request_master(0);
    if(master == NULL)
    {
        printf("EtherCAT master request failed!\n");
        return 1;
    }

    domain = ecrt_master_create_domain(master);
    if(domain == NULL)
    {
        printf("EtherCAT domain creation failed!\n");
        ret = 1;
        goto CLEANUP;
    }

    ret = ecrt_master(master, &master_info);
    if(ret != 0)
    {
        printf("EtherCAT master information request failed!\n");
        ret = -ret;
        goto CLEANUP;
    }

    /* allocate momory for slave information */
    slave_count = master_info.slave_count;
    slave_info_list = (ec_slave_info_t*)malloc(sizeof(ec_slave_info_t) * slave_count);

    /* configure slaves */
    for(i = 0; i < slave_count; i++)
    {
        ret = ecrt_master_get_slave(master, i, &slave_info_list[i]);
        if(ret != 0)
        {
            printf("EtherCAT slave information request failed!\n");
            ret = -ret;
            goto CLEANUP;
        }

        slave = ecrt_master_slave_config(master, 0, i, slave_info_list[i].vendor_id,
            slave_info_list[i].product_code);
        if(slave == NULL)
        {
            printf("EtherCAT slave configuration failed!\n");
            ret = 1;
            goto CLEANUP;
        }
    }

    /* setup PDO registration array */
    for(i = 0; pdo_entry_reg[i].index != 0; i++)
    {
        if(pdo_entry_reg[i].position < slave_count)
        {
            pdo_entry_reg[i].vendor_id =
                slave_info_list[pdo_entry_reg[i].position].vendor_id;
            pdo_entry_reg[i].product_code =
                slave_info_list[pdo_entry_reg[i].position].product_code;
        }
    }

    /* get PDO entry list */
    ret = ecrt_domain_reg_pdo_entry_list(domain, pdo_entry_reg);
    if(ret != 0)
    {
        printf("EtherCAT PDO registration failed!\n");
        goto CLEANUP;
    }

    /* create real-time periodic task */
    rt_task_plc = (RT_TASK*)malloc(sizeof(RT_TASK));
    ret = rt_task_create(rt_task_plc, "rt_task_plc", 0, 50, T_JOINABLE);
    if(ret != 0)
    {
        printf("Real-time task creation failed!\n");
        goto CLEANUP;
    }

    /* activate EtherCAT master */
    ret = ecrt_master_set_send_interval(master, INTERVAL);
    if(ret != 0)
    {
        printf("EtherCAT setting send interval failed!\n");
        ret = -ret;
        goto CLEANUP;
    }

    ret = ecrt_master_activate(master);
    if(ret != 0)
    {
        printf("EtherCAT master activation failed!\n");
        ret = -ret;
        goto CLEANUP;
    }

    /* get PDO domain pointer */
    domain_pd = ecrt_domain_data(domain);
    if(domain_pd == NULL)
    {
        printf("EtherCAT mapping process data failed!\n");
        ret = 1;
        goto CLEANUP;
    }

    /* start real-time periodic task */
    ret = rt_task_start(rt_task_plc, &rt_task_proc, rt_task_plc);
    if(ret != 0)
    {
        printf("Real-time task start failed!\n");
        goto CLEANUP;
    }
    rt_task_join(rt_task_plc);

CLEANUP :
    if(slave_info_list != NULL)
        free(slave_info_list);
    if(rt_task_plc != NULL)
        free(rt_task_plc);

    if(master != NULL)
        ecrt_release_master(master);

    return ret;
}

static void rt_task_proc(void *arg)
{
    int sw, led;
    int count = 0;

    RT_TASK* rt_task_plc = (RT_TASK*)arg;
    RTIME current_time = rt_timer_read();

    /* set real-time task timer */
    rt_task_set_periodic(rt_task_plc, current_time + INTERVAL,
        rt_timer_ns2ticks(INTERVAL));

    while(alive)
    {
        /* retrieve */
        ecrt_master_receive(master);
        ecrt_domain_process(domain);
        sw = EC_READ_BIT(domain_pd + slave0_6000_01, slave0_6000_01_bit);

        /* computation */
        if(sw)
        {
            if(++count >= 500)
            {
                led = !led;
                count = 0;
            }
        }
        else
        {
            led = 0;
            count = 0;
        }

        /* publish */
        EC_WRITE_BIT(domain_pd + slave0_7010_01, slave0_7010_01_bit, led);        ecrt_domain_queue(domain);
        ecrt_master_send(master);

        /* wait until next period */
        rt_task_wait_period(NULL);
    }
}

static void sigint_handler(int sig)
{
    alive = 0;
}


[설명]


  본 예제 코드는 slave 장치의 스위치 (OD index=0x6000, subindex=0x1로 정의된)가 on 상태인 경우 LED (OD index=0x7010, subindex=0x1로 정의된)를 1Hz (0.5초마다 출력값 토글)하는 응용이다.
  IgH와 같은 역할을 하는 SOEM을 활용한 마스터 예제 코드와 비교해서 IgH가 더 성능도 좋고 기능도 많기 때문인지 예제 코드의 양이 많은 편이다.


[마스터 초기화]


1  master = ecrt_request_master(0);
2  domain = ecrt_master_create_domain(master);
3  ecrt_master(master, &master_info);
4  slave_count = master_info.slave_count;

  마스터 초기화는 현재 프로그램이 실행되고 있는 마스터 장치에서 0번 EtherCAT 인터페이스에 대한 오프젝트(master)를 받아온 후 그에 대한 도메인 오프젝트(domain)을 받는 것으로 이루어진다. 마스터 오브젝트(master)가 마스터 장치를 의미한다면, 도메인(domain)은 주기적으로 통신할 데이터(PDO)들을 의미한다.
  마스터 오브젝트의 필드는 IgH 라이브러리 내에서만 접근할 수 있도록 감추어져 있기 때문에 슬레이브 장치의 수 등 정보를 알기 위해서는 3 line 처럼 마스터 오브젝트의 정보를 받아와야 한다. 슬레이브 장치의 수는 4 line과 같이 알 수 있다.


[슬레이브 초기화]


1  for(i = 0; i < slave_count; i++)
2  {
3      ecrt_master_get_slave(master, i, &slave_info_list[i]);
4      ecrt_master_slave_config(master, 0, i, slave_info_list[i].vendor_id,
           slave_info_list[i].product_code);
5  }

  슬레이브 초기화를 위한 함수 ecrt_master_slave_config()는 초기화하려는 슬레이브의 Vendor ID와 Product code를 필요로 한다. 해당 정보는 ecrt_master_get_slave() 함수로 얻어올 수 있다.
  본 예제와 같이 슬레이브로부터 필요한 정보를 받아와서 초기화를 하는 방법이 있고 마스터에서 미리 설정된 정보를 이용하여 직접 초기화 하는 방법이 있다. 전자의 경우 슬레이브 내에 설정 정보들이 저장되어 있는 SII(Slave Information Interface)로부터 마스터가 초기화에 필요한 정보를 읽어 초기화가 이루어지며, 후자의 경우 사용자가 특정 슬레이브의 정보가 XML 포맷으로 정의된 ESI 파일을 마스터 프로그램(예를 들어 TwinCAT과 같은 프로그램)을 통해 입력하여 초기화가 이루어진다.


[입출력(PDO) 설정]


1  static ec_pdo_entry_reg_t pdo_entry_reg[] = {
2      {0, 0, 0x0, 0x0, 0x6000, 1, &slave0_6000_01, &slave0_6000_01_bit},
3      {0, 0, 0x0, 0x0, 0x7010, 1, &slave0_7010_01, &slave0_7010_01_bit},
4      {}
5  };
6  ecrt_domain_reg_pdo_entry_list(domain, pdo_entry_reg);

  주기적으로 통신할 데이터를 정의하는 입출력 설정은 ec_pdo_entry_reg_t 타입의 구조체와 ecrt_domain_reg_pdo_entry_list() 함수가 사용된다. ec_pdo_entry_reg_t 구조체는 ecrt.h 파일에 다음과 같이 정의되어 있다.

typedef struct {
    uint16_t alias;
    uint16_t position;
    uint32_t vendor_id;
    uint32_t product_code;
    uint16_t index;
    uint8_t subindex;
    unsigned int *offset;
    unsigned int *bit_position;
} ec_pdo_entry_reg_t;

  alias와 position을 어떤 슬레이브 장치인지 특정한다. vendor_id와 product_code는 특정한 슬레이브에 대한 정보로, 해당 슬레이브의 정보와 일치해야 한다. 예제 코드에서는 ecrt_master_get_slave() 함수를 통해 얻은 정보를 이용하여 이 값을 초기화하는 코드가 있다. index와 subindex는 특정한 슬레이브 장치에서 통신할 OD(Object Dictionary)의 index와 subindex를 의미한다. 이와 같은 정보를 입력한 구조체의 배열을 2~3 line과 같이 정의한 후 ecrt_domain_reg_pdo_entry_list() 함수를 호출하면 해당 구조체의 마지막 두 필드에 PDO 엔트리의 byte offset과 bit position이 저장된다. 이 값은 추후에 입출력을 위해 사용된다.


[마스터 활성화]


1  ecrt_master_set_send_interval(master, INTERVAL);
2  ecrt_master_activate(master);
3  domain_pd = ecrt_domain_data(domain);

  마스터가 통신할 주기를 설정하고 활성화한다. 통신할 주기는 ecrt_master_set_send_interval() 함수를 통해 설정할 수 있으며, 두 번째 인자에 주기를 나노초 단위로 입력하면 된다. 이후에 ecrt_master_activate() 함수를 통해 마스터 장치를 활성화한다. 활성화 이후에는 실시간 context로 전환되며 malloc()이나 대부분의 IgH 라이브러리 함수들을 포함하는 비실시간 함수들의 사용은 제한된다. 이 함수가 호출된 이후에는 실시간 함수들만 사용할 수 있다.
  ecrt_domain_data() 함수는 인자로 전달된 도메인에 대한 데이터 필드의 포인터를 받아온다. 이 값도 추후에 입출력을 위해 사용된다.


[실행 단계]


1   while(alive)
2   {
3       /* retrieve */
4       ecrt_master_receive(master);
5       ecrt_domain_process(domain);
6       sw = EC_READ_BIT(domain_pd + slave0_6000_01, slave0_6000_01_bit);
7
8       /* computation */
9       /* write some computation code... */
10
11      /* publish */
12      EC_WRITE_BIT(domain_pd + slave0_7010_01, slave0_7010_01_bit, led);
13      ecrt_domain_queue(domain);
14      ecrt_master_send(master);
15
16      /* wait until next period */
17  }

  실행 단계에서는 ecrt_master_set_send_interval() 함수를 통해 설정한 주기마다 [retrieve] - [computation] - [publish]를 반복한다.
  retrieve에서는 ecrt_mater_receive() 함수와 ecrt_domain_process() 함수가 기본적으로 필요하며, 앞서 입출력 설정에서 입력으로 설정한 데이터를 슬레이브로부터 받아오는 것으로 구성된다. 데이터를 받아오는 기능은 ecrt.h에 매크로로 정의되어 있으며 데이터의 타입별로 매크로가 따로 정의되어 있다. (EC_READ_BIT 등) 이 매크로를 사용할 때 마스터 활성화 단계에서 얻은 도메인 데이터 필드의 포인터(domain_pd)와 함께 입출력 설정에서 ecrt_domain_reg_pdo_entry_list() 함수를 통해 얻은 byte offset과 bit position이 사용된다.
  computation에서는 retrieve에서 읽어오는 데이터를 토대로 publish에서 쓸 데이터를 계산한다. 이 위치의 코드는 응용에 따라 자유롭게 구성된다.
  publish에서는 retrieve와 반대로 먼저 EC_WRITE_BIT 등의 매크로로 도메인 데이터 필드에 값을 쓴 다음 ecrt_domain_queue() 함수와 ecrt_master_send() 함수를 통해 그 값을 슬레이브로 출력한다.