[컴][안드로이드] android studio 에서 ndk hello world 작성하기




Android studio 에서 NDK 사용하기 from google

 여기는 gradle 자체를 변경해서 사용한다. 훨씬 간단해 보이기는 하나, 아직 실험중인 것이라, 뭐라 이야기 하기 그렇다. 추가로 native 함수 debugging 하는 부분이 있다.


kotlin 지원

그리고 개인적으로kotlin 을 사용하는데, 아직은 experimental 에서 지원을 못하는 듯 하다. 아래 글과 같은 error 가 난다.



Anroid studio 에서 NDK 사용하기

Android studio 에서 NDK 를 사용해 보자. 이 글은 아래 동영상의 내용보면서 작성한 글이다.




android.useDeprecatedNdk=true

새로운 버전에서 NDK integration 이 exprimental gradle 로 넘어가서, 현재 gradle 에선 deprecated 시켰다고 한다. 그래서 아래 설정을 gradle.properties 에 넣어줘야 한다.
android.useDeprecatedNdk=true

class 에 jni definition 넣고 rebuild

class 에 jni 정의 를 넣어주고, project 를 rebuild 한다.(이 때 생긴 class 를 이용해서 javah 를 실행하게 된다.)(소스 참고)
public native String stringFromJNI();


javah 실행

rebuild project 를 해준다. 그리고 아래 명령어를 실행해 주자.

D:\mine\programming\androidStudio\AndroidSamples\NdkHelloWorld\app\src\main>"c:\Program Files\Java\jdk1.7.0_25\bin\javah.exe" -d jni -classpath "D:\Program Files\Android\sdk\platforms\android-19\a
ndroid.jar";"d:\Program Files\Android\sdk\extras\android\support\v7\appcompat\libs\android-support-v7-appcompat.jar";"d:\Program Files\Android\sdk\extras\android\support\v7\appcompat\libs\android-
support-v4.jar";..\..\build\intermediates\classes\debug com.namh.ndkhelloworld.MainActivity

위의 코드를 좀 더 보기 좋게 변환하면 아래와 같다.

set javah=c:\Program Files\Java\jdk1.7.0_25\bin\javah.exe
set classpath=d:\Program Files\Android\sdk\platforms\android-23\android.jar
set v7appcompat=d:\Program Files\Android\sdk\extras\android\support\v7\appcompat\libs\android-support-v7-appcompat.jar
set v4appcompat=d:\Program Files\Android\sdk\extras\android\support\v7\appcompat\libs\android-support-v4.jar
set classname=com.namh.ndkhelloworld.MainActivity


"%javah%" -d jni -classpath "%classpath%";"%v7appcompat%";"%v4appcompat%";..\..\build\intermediates\classes\debug %classname%

그러면 jni folder 가 생성된다.



.c 만들기

이 header 파일중에 우리는 MainActivity.h 에 jni 함수정의를 넣어놨으니, *_MainActivity.h 를 확인하면 함수정의를 찾을 수 있다. 이 함수정의를 이용해서 이제 .c 를 만들자.

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

#ifndef _Included_com_namh_ndkhelloworld_MainActivity
#define _Included_com_namh_ndkhelloworld_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
#undef com_namh_ndkhelloworld_MainActivity_MODE_PRIVATE
#define com_namh_ndkhelloworld_MainActivity_MODE_PRIVATE 0L
#undef com_namh_ndkhelloworld_MainActivity_MODE_WORLD_READABLE
#define com_namh_ndkhelloworld_MainActivity_MODE_WORLD_READABLE 1L

...
...
...

/*
 * Class:     com_namh_ndkhelloworld_MainActivity
 * Method:    stringFromJNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_namh_ndkhelloworld_MainActivity_stringFromJNI
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

아래처럼 .c 를 만들어 줬다.

// hello.c
#include "com_namh_ndkhelloworld_MainActivity.h"

JNIEXPORT jstring JNICALL Java_com_namh_ndkhelloworld_MainActivity_stringFromJNI
  (JNIEnv * env, jobject obj)
  {
    return (*env)->NewStringUTF(env, "hello jni !");
  }


No rule to make target

.c 가 한개밖에 없을 때 "No rule to make target" 를 내뿝는다. 이게 windows ndk bug 라고 하니, 빈 .c 파일을 하나 만들어서 추가하자.



local.properties 에 ndk.dir 추가

이제 ndk 경로를 추가해 주자. <project_path>/local.properties 에 ndk.dir 을 추가해 주면 된다. 단, 주의할 것은 공백(space) 가 있는 경로는 안된다.[ref. 2]
추가로 안드로이드 스튜디오에서 NDK tool 에서 문제가 발생할 수 있어서 sdk 의 path 에도 공백이 없게 하라고 나온다.
sdk.dir=D\:\\Program Files\\Android\\sdk
ndk.dir=D\:\\Android\\ndk\\android-ndk-r10d


library 를 application 이 시작할 때 load 하기 위해서 activity class 에
static {
        System.loadLibrary("hello-jni");
    }
를 넣어주자. 이 때 "hello-jni" 는 library 의 이름이 되는데, 이녀석은 app/build.gradle 에 정의해 줘야 한다.

apply plugin: 'com.android.application'

android {
    ...

    defaultConfig {
        applicationId "com.namh.ndkhelloworld"
        minSdkVersion 16
        targetSdkVersion 21
        versionCode 1
        versionName "1.0"

        ndk{
            moduleName "hello-jni"
        }
    }
    ...

}

제대로 compile 이 되면 아래 그림처럼 .so 들이 만들어진다.


이제 Make project 를 다시 실행하면 된다.

이제 실제 동작하는지 activity 에서 jni 함수를 넣은 code 를 작성해서 한 번 실행 해 보면 된다.



Android.mk / Application.mk


Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := hello-jni
LOCAL_SRC_FILES := hello.c
LOCAL_LDLIBS := -llog

include $(BUILD_SHARED_LIBRARY)

Application.mk

# The ARMv7 is significantly faster due to the use of the hardware FPU
#APP_ABI := armeabi armeabi-v7a
APP_ABI := x86_64 armeabi armeabi-v7a





Custom Android.mk 사용하기

jni.srcDirs=[]

ref.4 를 보면 gradle Android plugin 에서 convention 으로 잡혀있는 path 를 바꿀 수 있는데, jni 도 그렇게 바꿀 수 있다.

Build project 를 실행할 때 jni 가 folder 가 있으면 자동으로 NdkCompile task(code : NdkCompile.groovy) 를 실행되게 되는데,[ref. 2]
  • jni.srcDirs = []

를 하면 jni 가 없다는 뜻이 되고, jni 에 대한 compile(NdkCompile) 을 시도하지 않을 것이다.

그러면 원래 NdkCompile 이 ndk-build 를 수행하면서 만드는 아래의 파일들을 만들지 않게 된다.
  • <project_path>\app\build\intermediates\ndk\debug
  • 자동으로 만들어지는 파일
    • Android.mk
    • lib/<eabi_folder>/libhello-jni.so

android{
  sourceSets.main {
        jniLibs.srcDir 'src/main/libs' // 이녀석을 넣지 않아도 될 듯 하다. 불확실.
        jni.srcDirs = []    //disable automatic ndk-build call with auto-generated Android.mk
    }



tasks.withType(JavaCompile)

tasks.withType(JavaCompile) 을 추가해 주자. 이 부분에서 compile 시에 ndkLibsToJar 이 호출되도록 dependency 를 추가해 주게 된다.

참고로 이전에는 tasks.withType(Compile) 도 됐는데, 현재는 안된다. ref. 5를 참고하자.

tasks.withType(JavaCompile) {
    compileTask -> compileTask.dependsOn ndkLibsToJar
}


ndkLibsToJar

이제 ndkLibsToJar task 가 필요하다. ndkLibsToJar 은 ref. 2에서 가져왔다. 여기에 보면 dependsOn 에 'ndkBuild' 가 있다. 이제 ndkBuild task 를 만들자.

task ndkLibsToJar(type: Zip, dependsOn: 'ndkBuild', description: 'Create a JAR of the native libs') {
    destinationDir new File(buildDir, 'libs')
    baseName 'ndk-libs'
    extension 'jar'
    from(new File(buildDir, 'libs')) { include '**/*.so' }
    into 'lib/'
}


ndkBuild

task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
    println('executing ndkBuild')
    commandLine "echo", "hello world"
}


ndkBuild 가 잘 동작하는지 확인하는 차원에서 ref. 2 에서 처럼 hello world 를 한 번 찍어보자. 이부분을 나중에는 아래처럼 수정할 것이다.

task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
    def ndkDir = project.plugins.findPlugin('com.android.application').getNdkFolder()
    println(ndkDir)
    commandLine "$ndkDir/ndk-build.cmd",
            'NDK_PROJECT_PATH=build',
            'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
            'NDK_APPLICATION_MK=src/main/jni/Application.mk'
}

이제, Build > Make Project 를 하자.


위처럼 executing ndkBuild 는 task 를 define 하는 시점에 실행되고, commandLine 부분은 실제 task 가 실행되는 시점에 실행된다.[ref. 2] 그렇기 때문에 위와 같은 결과화면이 나온다.

이제 진짜 ndkBuild 부분을 넣고 Make project 를 실행하면 된다.

def getNdkBuildPath() {
    Properties properties = new Properties()
    properties.load(project.rootProject.file('local.properties').newDataInputStream())

    def command =  properties.getProperty('ndk.dir')
    return command
}


android {
    ...// For Ndk-build
    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn ndkBuild
    }
    task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
        def ndkDir = getNdkBuildPath()
        println(ndkDir)
        commandLine "$ndkDir/ndk-build.cmd", '-C', file('src/main').absolutePath
    }


}






source git




------------------------------------------------

gradle 을 이용해서 architecture 별로 build

이제 architecture 별로 build 할 수 있는 법을 살펴보자. 동영상은 아래 link 를 확인하면 된다.







source 는 ndk 의 sample source 를 참고했다.


<ndk_path>\android-ndk-r10d\samples\hello-jni\src\com\example\hellojni\HelloJni.java




  1. Android Project 만들기
  2. terminal 에서 javah 실행
  3. copy <hello-jni>\jni ---> <NdkHelloWorld>\app\src\main\jni
  4. <NdkHelloWorld>\app\src\main\libs 만들기 :ndk-build에서 출력 디렉토리를 설정할 수가 없기 때문에 결과 파일이 src->main->libs 에 생긴다[ref. 1]
  5. <NdkHelloWorld>\app\src\main\libs 아래 eabi platform 별로 directory 만들기
  6. <NdkHelloWorld>\local.properties 에 ndk.dir 추가
  7. <NdkHelloWorld>\app\build.gradle 에 ndk 관련 설정 추가(설명은 ref. 2에서 확인할 수 있다.)
    android {
        ...
    
        ndk {
            moduleName "hello-jni"
        }
    
    
        sourceSets.main{
            jni.srcDirs = []
            jniLibs.srcDir 'src/main/libs'
        }
     
        task buildNative(type: Exec, description: 'Compile JNI source via NDK') {
            def ndkDir = project.plugins.findPlugin('com.android.application').getNdkFolder()
            commandLine "$ndkDir/ndk-build",
                    '-C', file('src/main/jni').absolutePath,
                    '-j', Runtime.runtime.availableProcessors(),
                    'all',
                    'NDK_DEBUG=1'
        }
     
        task cleanNative(type: Exec, description: 'Clean JNI object files') {
            def ndkDir = project.plugins.findPlugin('com.android.application').getNdkFolder()
            commandLine "$ndkDir/ndk-build",
                    '-C', file('src/main/jni').absolutePath,
                    'clean'
        }
         
        clean.dependsOn 'cleanNative'
         
        tasks.withType(JavaCompile) {
            compileTask -> compileTask.dependsOn buildNative
        }
    
        
    }
    




주의할 점

ndk 를 만들 때 이미 어느 class 에서 쓰여질지 정해진다. javah 를 실행할 때 말이다. 그러니 만약에 사용하고 싶은 class 를 옮기려고 한다면 javah 를 다시 만들어야 한다. 그렇지 않으면 아래의 에러가 날 것이다.

FATAL EXCEPTION: main
Process: com.namh.ndkhelloworld2, PID: 26773
java.lang.UnsatisfiedLinkError: No implementation found for java.lang.String com.namh.ndkhelloworld2.NDKLib.getStringNative() (tried Java_com_namh_ndkhelloworld2_NDKLib_getStringNative and Java_com_namh_ndkhelloworld2_NDKLib_getStringNative__)
   at com.namh.ndkhelloworld2.NDKLib.getStringNative(Native Method)
   at com.namh.ndkhelloworld2.MainActivity.onCreate(MainActivity.java:34)





See Also

  1. JNI 동작원리 개념 - 1
  2. Android Studio Project에 NDK 적용하기 (Part 1) | David Lab 
  3. Android Studio Project에 NDK 적용하기 (Part 2) | David Lab
  4. fuzzdota/kotlin-jni-example · GitHub 



References

  1. ndk-build를 Android Studio에서 사용하기 - Burt K.
  2. Using custom Android.mk with Gradle/Android Studio
  3. Android Studio, gradle and NDK integration | ph0b's
  4. Android Tools Project Site > 3.2.1 Configuring the Structure
  5. After upgrading to Gradle 2.0: Could not find property 'Compile' on root project

댓글 없음:

댓글 쓰기