요즘 소프트웨어 개발하는 사람치고 아이폰이나 안드로이드 개발과 전혀 무관할 수 있는 분이 드물텐데요. 그런데 갑좌~기 회사에서 떨어진 프로젝트가 기존에 만들어서 사용중인 C++ 코드를 안드로이드로 마이그레이션 하는 일이라면? 아 이거 참 만만찮게 난감한 일일텐데요. 월간 마이크로소프트웨어의 필자 이상욱 님이 이런 난감한 상황에 대처할 수 있는 방법을 재미있게 설명한 글이 있어 옮겨봅니다. 아직 정식으로 인사 드리진 못했는데 조만간 한번 꼭 뵙고 싶어지는 분이네요. 좋은 글 주신 상욱 님께 감사드립니다. 그리고 이 글이 많은 개발자분들께 도움이 되길 바라겠습니다.
출처| 아이마소
안드로이드 네이티브 라이브러리Ⅰ
이번 컬럼에서는 기존에 C/C++ 로 개발한 코드가 안드로이드 플랫폼에서 어떻게 동작하는지 안드로이드 플랫폼 구조를 통해 알아보고, Java 응용프로그램과 연동하기 위한 다양한 방법을 소개한다. 또 C/C++ 코드를 안드로이드 플랫폼에서 동작하는 바이너리로 빌드할 수 있는 툴체인 안드로이드 NDK에 대해 알아보고 간단한 사용법에 대해 알아보도록 하겠다.
이상욱 bumwoogi@gmail.com|새로운 기술을 배우는 것을 좋아하고 다방면에 관심이 많은 오지랍쟁이 개발자. 얼마전까지 무인잠수함을 만드는 것을 계획하였으나 도중하차 하였고, 지금은 전 인류에 공헌할 대단한 소프트웨어를 만드는 것을 꿈꾸고 있다.
이번 컬럼은 가상의 시나리오를 통해 실제 프로젝트 진행 중에 벌어질 수 있는 문제상황을 제시하고 동시에 이 문제를 해결해나가는 과정을 알아보도록 하겠다.
등장인물
- 허우대 대리 : 몇달전 경력사원 공채로 새로 입사한 인물로 Java를 사용해봤다는 이유만으로 난생 처음으로 안드로이드 플랫폼에 기존에 C++로 작성한 소스를 마이그레이션 하는 업무를 맞는다. 허우대는 멀쩡 하지만 성과가 없다고 일정만 차장으로 부터 허우대라는 별명을 얻었다.
- 신익한 선배 : 허우대 대리의 학교 선배로 모바일 분야에서 안해본게 없는 대단한 실력자 이지만, 일을 너무 사랑하는 나머지 인간관계는 별로 소질이 없어 보이는 시니컬한 성격의 소유자, 허우대 대리의 질문에 매번 퉁명스럽게 대답 하지만 그 속마음에는 후배를 스스로 깨우치게 하려는 나름의 철학을 가지고 있다.
- 일정만 차장 : 이번 프로젝트의 책임자로, 일에 대한 책임감이 투철하지만 상냥한 성격은 아니다. 특히 일정을 어기는 사람을 무척 싫어한다.
허우대 대리는 얼마전 일정만 차장으로 부터 1주일간의 시간동안 이전 누군가가 C++ 로 작성한 방대한 양의 xml 문서 parser를 이번 프로젝트에 재사용 할 수 있도록 안드로이드 플랫폼에 마이그레이션 하라는 지시를 받았다. 허우대 대리에게 주어진 시간은 고작 7일. 7일 동안 허우대 대리가 어떻게 이 프로젝트를 진행하는지 함께 지켜보도록 하자.
선배, 안드로이드 애플리케이션 개발은 Java로 하는거지?
생전 처음 안드로이드 플랫폼에서 개발을 시작하는 허우대 대리는 신익한 선배를 찾아가서 조언을 구하게 되었다.
“선배, 차장님께 안드로이드 플랫폼에 기존에 c++로 작성한 엄청난 양의 parser 소스를 마이그레이션 하라는 지시를 받았는데, 나 이거 다 java로 다시 만들어야 하는건가?”
선배는 한심하다는 반응으로 특유의 시니컬한 한숨을 내쉬며 이렇게 얘기한다.
“안드로이드= java의 공식은 아니야. 안드로이드 플랫폼이 어떻게 구성돼있는지 먼저 확인해 보는게 어때?”
안드로이드 플랫폼은 크게 리눅스 커널과, C/C++로 작성한 라이브러리(보통 네이티브 라이브러리라 부른다), 안드로이드 응용프로그램의 뼈대를 제공하는 애플리케이션 프레임워크와 기본 애플리케이션으로 구성돼 있다.
리눅스 커널은 OS의 기본 기능인 태스크간의 스케줄링과 메모리, 디스크, 네트워크 등의 자원을 사용할 수 있도록 시스템 콜을 제공한다. 이는 리눅스 커널 윗부분에 초록색으로 표시된 네이티브 라이브러리를 통해 응용프로그램이 편하게 사용 할 수 있는 형태로 제공된다.
안드로이드 응용프로그램은 애플리케이션 프레임워크(응용프로그램의 구조를 제공하고 제작에 도움을 주는 Java class로 구성)을 이용하고 역시 Java언어로 작성한다.
이 Java파일을 안드로이드 SDK를 이용해 빌드하면 내부적으로 Java컴파일러와 dex converter를 거쳐 Dalvik VM(안드로이드 응용프로그램을 동작시키는 VM으로 일반적인 Java VM과는 다르다.) 위에서 동작하는 byte 코드로 변환하게 된다.
결론적으로 안드로이드 응용프로그램(Java로 작성)은 애플리케이션 프레임워크(.jar형태의 Java class.)를 이용해 VM에서 제공하는 코어 라이브러리(core library) 기능을 사용하게 되고, 이 코어 라이브러리는 리눅스 커널 위에서 동작하는 다양한 C/C++ 라이브러리(c/c++)를 호출하게 된다. 이 라이브러리는 필요에 따라 리눅스 커널의 system call을 호출하게 된다. 위 그림에서 화살표 방향을 따라 가면서 확인하자.
위 그림에서 안드로이드 런타임 계층과 라이브러리 계층을 연결하는 왼쪽 화살표로 표현된 부분에서 일반적인 함수호출 관계 외에 추가적으로 C/C++ ↔ java 호출 사이의 ‘glue’가 존재한다. 결국 이 덕분에 안드로이드 프레임워크는 자연스럽게 OS의 복잡한 기능을 리눅스 커널로부터 빌려 쓸 수 있는 것이다.
허우대 대리는 결국 C/C++ ↔ Java사이의 ‘glue’를 만들면 기존 소스를 리눅스 상에 포팅 하는 정도의 노력으로 재사용 할 수 있겠다는 결론을 내리고, 곧바로 Java ↔ C/C++ 를 호출 할 수 있는 방법을 찾아보기 시작했다.
Java ↔ C/C++ 사이의 glue는 어떤것을 사용할까?
일반적으로 Java에서 C/C++ 모듈을 사용하기 위해서는 다음과 같은 방법을 사용한다.
1. Java Native Access (JNA)
자바 코드에서 네이티브 코드와 같은 형태로 네이티브 함수를 호출하는 방식으로 기존 코드의 최소한의 수정으로 응용이 가능해, 최상의 방법으로 생각되나 현재 안드로이드 Dalvik VM에서 지원하지 않는 방법이다.
2. Java Native Interface (JNI)
Java 초창기부터 존재한 Java/Native interface 방법으로 안드로이드 Dalvik VM에서 지원하는 방법으로 실제 안드로이드 플랫폼 소스 여러 부분에서 사용되고 있다. 그러나 이 방식은 raw form을 다루는 형태로 개발해야 하기 때문에 개발 시간이 많이 소요되고 에러를 발생 시킬만한 요소가 많다.
3. Simplified Wrapper and Interface Generator (SWIG)
JNI를 좀 더 쉽게 사용 할수 있도록 해주는 도구로 C/C++ 인터페이스를 정의한 파일을 입력 받아 C/C++ JNI wrapper 코드를 생성한다. 이 방법 역시 Dalvik VM과는 호환성 문제가 존재한다.
Native library(C/C++) 로 개발할 것 인지에 대한 선택 기준
안드로이드에서는 J2SE의 대부분의 기능과, 애플리케이션 프레임워크를 통해 다양한 API 셋을 제공하고, Java를 사용하여 대부분의 응용프로그램을 제작 하는데 부족함이 없도록 배려하고 있다.
신규 개발시 애플리케이션 프레임워크를 통해 제공받을 수 없는 일부 기능(대부분 외부 기기 나 디바이스 종속적인) 부분과 성능이 아주 중요한 일부 기능을 제외하고는 Java를 사용하여 개발하는 것이 프로젝트 장기적인 관점에서는 나은 판단이라 생각된다.
C/C++로 코드로 개발은 유지보수와 디버깅 측면에서 번거롭고 불편한 부분이 있으며 JNI를 이용하여 Java 쪽으로 glue를 만드는 일이 예상하는 것만큼 간단한 일은 아니기 때문이다. 여기서 제시하는 가상의 시나리오 역시 주제를 이끌어 가기 위해 단순화시킨 예일 뿐이지 실제로 기존에 C/C++로 작성한 코드를 마이그레이션 하는 작업이 주어질 경우 얼마나 인터페이스가 단순한지, 기존코드에서 플랫폼 종속적인 부분이 얼마나 있으며 이것을 안드로이드(Linux kernel)에 포팅하기 위해서 얼마나 노력이 필요한지, 혹은 HTTP나 STL등 안드로이드 Native library단에서 충분히 제공하지 않는 기능을 사용한 부분이 있어서 재작업이나 추가 포팅 작업이 필요한지 충분히 고려하여 기존 모듈을 재활용할 것인지, Java코드로 다시 작업할 것인지 결정해야 할 것 이다.
허우대 대리는 Dalvik VM과의 호환성 문제와 보편적으로 사용하는 기술이라는 점을 감안해 JNI를 이용해 프로젝트를 진행하기로 했다.
점심시간에 허우대 대리는 자신이 JNI를 이용하여 기존 parser소스를 거의 재활용 할수있는 방법을 생각해 냈다고 신익한 선배에게 자랑을 늘어놓았다. 이때, 밥을 먹던 신익한 선배는 이런 이야기를 남긴 뒤, 아무일도 없었다는 듯 식사를 계속 했다. 신익한 선배가 남긴 말은 이렇다.
“그럼 안드로이드 시스템에 포함해서 빌드 하겠다는 얘기야? 배포는 어떻게 할건데?”
그렇다. 그림에서 보는 C/C++ 라이브러리는 안드로이드 시스템의 영역으로 허우대 대리는 배포방법에 대해 고려하지 못한 것이다. 안드로이드 에뮬레이터를 실행하고 <화면 1>에서 초록색으로 표시한 C/C++ 라이브러리 파일의 실제 위치를 확인해보자. 아래와 같이 커맨드 창에서 emulator를 실행하거나 이클립스 IDE를 통해 emulator를 실행한다.
에뮬레이터가 정상적으로 실행되면 위와 같이 에뮬레이터 화면을 확인할 수 있는데, 이때 콘솔에서는 <리스트 1>과 같이 adb 명령을 이용하여 shell을 실행한다.
<리스트 1> adb shell을 이용하여 네이티브 라이브러리 확인하기
----------------------------------------------------------------------------------------------------------
>adb shell
adb shell 이 실행되면 아래와 같이 library path를 확인해보자
#echo $LD_LIBRARY_PATH
#/system/lib
system/lib가 library path로 잡혀있는 것을 확인하고 /system/lib directory로 이동한다.
#cd /system/lib
----------------------------------------------------------------------------------------------------------
<리스트 1>의 내용을 <화면 3>를 통해 확인해보자.
<화면 3>에서는 안드로이드 플랫폼에서 제공하는 네이티브 라이브러리가 위치하는 것을 확인할 수 있다.
허우대 대리가 준비하는 프로젝트의 결과물은 다운로드 가능한 형태로 배포되어야 하는데 /system 디렉토리는 다운로드한 애플리케이션이 설치할 수 있는 영역이 아니기 때문에 /system 디렉토리 대신 /data 디렉토리에서 동작 할 수 있는 방법을 찾아야 한다.
안드로이드 시스템 이미지
안드로이드 시스템 이미지는 리눅스 운영체제에서 파일시스템은 단순히 자료의 저장기능 뿐 아니라 운영체제 동작에 필요한 필수 정보를 포함하고 장치 관리 등의 특별한 용도로도 사용하므로 선택항목이 아니라 필수 항목이다. 시스템 이미지는 안드로이드 프레임워크를 구동하기 위해 필수적인 항목으로 <안드로이드 SDK설치 디렉토리>/platforms/<플랫폼버전>/images/system. img 에서 확인 할 수 있다.
이 이미지파일은 실제 에뮬레이터가 구동될 때 /system directory에 마운트하게 되는데, 이내용은 adb shell을 실행시킨 후 최상위 디렉토리의 init.rc파일에서 확인 할 수 있다.
/system 디렉토리는 임베디드하여 탑재할 기본 응용프로그램을 포함해 안드로이드 프레임워크에 필요한 라이브러리 설정파일 등이 들어있는 영역으로 추가로 다운로드받은 응용프로그램과는 구별된다. 다운로드 받은 응용프로그램은 /data 디렉토리에 위치하게 된다.
안드로이드 SDK 설치
앞서 설명된 부분들을 실습하기 위해서는 안드로이드 SDK를 설치하고 버추얼 디바이스를 생성해야 하는데, 이 부분은 이번 컬럼의 주제와는 벗어나므로 아래 URL을 참고해 설치하기 바란다.
http://developer.android.com/sdk/index.html
허우대 대리, NDK와 만나다
다음날 배포문제로 고민하던 허우대 대리는 아래의 안드로이드 개발자 웹사이트를 통해 NDK에 대한 내용을 접했다.
http://developer.android.com/sdk/ndk/1.6_r1/index.html
NDK는 C/C++로 작성한 코드로 리눅스 OS 기반의 ARM binary를 생성하기 위한 크로스 툴체인을 제공한다. 여기서 ‘크로스 툴체인’이란 타깃머신과는 다른 환경에서 타깃 머신용으로 바이너리를 생성할 수 있도록 제공되는 툴인데 컴파일러 링커 그리고 기타 컴파일에 필요한 유틸리티를 포함하고 있다.
<리스트 2> Application.mk 파일
--------------------------------------------------------------
APP_PROJECT_PATH := $(call my-dir)/project
APP_MODULES := hello-jni
--------------------------------------------------------------
일반적인 x86호환 CPU의 윈도우나 리눅스 PC기반 환경에서 ARM이나 MIPS등 임베디드 머신의 CPU에서 동작하는 바이너리를 컴파일 할 수 있도록 해준다.
그리고 아래와 같은 라이브러리를 사용할 수 있도록 header file을 제공하는데, 이 내용은 앞서 제시된 adb 쉘에서 확인한 /system/lib 디렉토리에 있는 라이브러리의 일부임을 확인할 수 있다.
- libc(C library)
- libm(math library)
- JNI interface
- libz (zip 압축 library)
- liblog(Android logging library)
- OpenGL ES library (3D graphic library, NDK 1.6버전부터)
추가적으로 안드로이드 응용프로그램 패키지 파일(.apk)에 native library를 포함할 수 있는 방법을 제공한다. 허우대 대리가 찾던 바로 그 방법이다. 그럼 이제 허우대 대리와 함께 NDK를 설치해 보도록 하자.
(1)
http://developer.android.com/sdk/ndk/1.6_r1/index.html 에서 윈도우용 패키지를 다운로드 받는다.
(2) 적당한 위치에 (1)에서 다운로드 받은 압축파일을 푼다.
(3) www.cygwin.com에서 최신 cygwin 을 설치한다.
(4) 설치한 Cygwin bash shell을 실행하고 bash shell에서 /cygdrive/<NDK 설치 경로> 로 이동해 ‘Build/host-setup.sh’ 라고 명령을 수행한다.
이처럼 순차적으로 따라하고 나면, <화면 4>와 같이 ‘Host setup complete…’라는 문구가 나타나는데, 이럴 경우 셋업이 성공한 것이다.
이제 설치가 끝났으니 먼저 NDK에 샘플로 제공되는 hello-jni 예제를 빌드해 보자. 빌드시에는 $make APP=hello-jni라고 명령을 수행한다. 재빌드시에는 -B 옵션을 사용하고 실제 build command를 모두 확인하고 싶은 경우 V=1 옵션을 사용한다.
빌드결과로 apps/hello-jni/project/libs/areabi/libhello-jni.so가 생성되었는지 확인하자.
어떻게 NDK에서 제공하는 툴을 이용하여 예제가 빌드 되는지 알고 싶다면, apps/hello-jni/Application.mk 와 sources/ hello-jni/Android.mk 파일을 열어보자.
일반적인 make 파일과 같은 형태이며 빌드를 위한 변수를 설정하는 부분을 확인 할 수 있다. 또한 Application.mk 파일에는 응용프로그램 에서 어떤 네이티브 라이브러리 모듈을 사용할 것인지에 대해 기술해야 한다. 이 부분은 네이티브 라이브러리를 빌드하는 것과는 직접적인 관계가 없으나 이 파일을 참조해 빌드할 타깃 모듈을 결정하기 때문에 필수적으로 작성해야 한다.
<리스트 3> Android.mk 파일
----------------------------------------------------
… 중략…
LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.c
include $(BUILD_SHARED_LIBRARY)
----------------------------------------------------
Android.mk 파일에는 실제 shared library로 빌드할 c/c++ 파일 목록 과 사용하는 라이브러리 목록, compile/link option 등을 기술해야 하는데, 이때 변수 LOCAL_MODULE 에 모듈이름을 기술하게 된다. 이어 변수 LOCAL_SRC_FILES 에 빌드하려고 하는 소스 파일명을 기술하고, 파일명 사이는 스페이스로 구별한다.
마지막 줄이 shared library, 즉 .so 파일로 빌드 하겠다는 뜻이다. 이렇게 기술하면 자동으로 lib+모듈명+.so 형태의 파일을 빌드하게 된다.
추가로 특정 라이브러리를 사용하기 위해서는 아래와 같이 LOCAL_LDLIBS 변수에 내용을 기술한다. 아래는 안드로이드 로그 라이브러리를 사용하기 위한 예이다. 또한 이에 대한 추가적인 내용은 NDK document 폴더의 ‘ANDROID-MK.TXT’,’APPLICATION-MK.TXT’,’HOWTO.TXT’ 문서를 참고하기 바란다.
LOCAL_LDLIBS :=-L$(SYSROOT)/usr/lib/ -llog
이제 네이티브 라이브러리 빌드가 끝났으니 네이티브 라이브러리를 사용하는 Java 코드를 살펴보자. <화면 6>과 같이 apps/hello-jni 프로젝트를 이클립스에서 열어보도록 하자. 왼쪽 패키지 익스플로러 창에서 마우스 오른쪽 버튼을 클릭한 뒤 Import 메뉴를 선택한다.
이어 Existing Projects into Workspace를 선택한후 app/hello-jni/ 디렉토리를 선택한다. 이를 실행하면 <화면 7>과 같이 Package Explorer에 HelloJni 프로젝트가 로딩되면 성공한것이다.
이클립스 메뉴에서 Run->Run을 선택하거나 단축키로 Ctrl+F11을 선택하면 에뮬레이터에서 Hello-jni 프로젝트의 결과를 확인할수 있다.
에뮬레이터가 실행되는데 제법 시간이 걸리므로 인내심을 가지고 기다리기 바란다. 에뮬레이터를 동작하기 위해서는 플랫폼 버전에 맞는 AVD 파일을 미리 생성해야 하는데 이 내용은 http://developer.android.com/guide/developing/eclipse-adt.html을 참고하길 바란다.
더불어 이전에 adb shell 명령을 이용한 안드로이드 파일시스템을 살펴보았는데, 이번에는 이클립스 IDE에서 DDMS툴을 이용하여 손쉽게 파일시스템을 확인해보자. 이때 이클립스 툴바 오른편에서 DDMS perspective 버튼을 선택한다. 오른편에 File Explorer 창을 이용하여 data/data/com.example.hellojni 폴더의 내용을 확인할 수 있으며, lib폴더아래에 libhello-jni.so 파일이 있는 것 또한 확인할 수 있다.
<화면 9>에서 보여지듯, 결국 안드로이드 응용프로그램은 system/lib 폴더의 라이브러리 이외에 /data/data/<자신의 package이름>/lib 아래 네이티브 라이브러리를 추가로 참고하는 것을 확인할 수 있다.
네이티브 라이브러리를 사용하는 Java 코드 맛보기
허우대 대리는 Java코드 에서 어떻게 C로 작성한 네이티브 코드를 참고하는지 궁금해졌다. 이번에는 허우대 대리와 함께 src/com/example/HelloJni.java 코드를 살펴보자.
<리스트 4> HelloJni.java 파일
-----------------------------------------------------------------
… 중략…
public class HelloJni extends Activity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
TextView tv = new TextView(this);
tv.setText( stringFromJNI() ); //-- (3)
setContentView(tv);
}
public native String stringFromJNI(); //-- (1)
… 중략…
static {
System.loadLibrary("hello-jni"); //-(2)
}
}
-----------------------------------------------------------------
<리스트 4>는 stringFromJNI 함수를 호출하여 얻은 문자열을 textView에 내용으로 넣어주는 간단한 소스로, 3가지 의미로 이용된다.
1) native 키워드를 붙혀서 선언부만 작성한다. 네이티브 라이브러리에 구현부가 있음을 알려준다.
2) static {..} 부분은 이 class가 로딩될 때 호출되는 되는데 이때 ‘hello-jni’ 라는 이름의 네이티브 라이브러리를 로딩하라는 의미이다.
3) 네이티브 라이브러리 함수라도 보통의 Java 메소드와 같은 방식으로 사용한다.
이어 Source/samples/hello-jni.c 파일을 열어서 네이티브 라이브러리 구현부도 함께 살펴보도록 하자.
<리스트 5> 네이티브 라이브러리 구현부 코드
----------------------------------------------------------------------------------------------
Jstring Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
jobject thiz )
{
return (*env)->NewStringUTF(env, "Hello from JNI !")
}
----------------------------------------------------------------------------------------------
<리스트 5>를 보면 함수이름 앞에 ‘Java_com_example_ hellojni_HelloJni_’ 가 붙어 있는데, 이 부분은 JNI 함수이름 규칙에 의해 package명_클래스명이 Java에서 선언한 함수 이름 앞에 붙게 된다. 함수의 내용은 단순히 “Hello from JNI!” 라는 문자열을 Java에서 사용하는 String 타입으로 변환하여 반환하는 내용이다.