Tutorial: Cómo ejecutar código C desde Android con el NDK

Este artículo brinda una introducción a la creación de aplicaciones nativas Android* (desarrolladas mediante NDK, Native Development Kit) para dispositivos basados en arquitectura Intel® (AI). Discutiremos también la exportación de aplicaciones Android NDK que hayan sido creadas para dispositivos con otras arquitecturas a dispositivos basados en AI. Recorreremos dos escenarios, uno para mostrar el proceso de creación de una aplicación Android* básica mediante NDK de principio a fin y el otro para exhibir un proceso simple de exportación de una aplicación existente Android basada en NDK a ser utilizada en dispositivos basados en AI.

Introducción

Las aplicaciones Android pueden incorporar código nativo mediante el conjunto de herramientas Native Development Kit (NDK). De esta manera, los desarrolladores pueden reutilizar código legado, hacer programación de bajo nivel hacia el hardware, o diferenciar sus aplicaciones aprovechando características que no es posible o práctico acceder con otros mecanismos.

Este artículo es una introducción básica sobre cómo crear aplicaciones basadas en NDK para arquitectura Intel (AI) de principio a fin. Recorreremos paso a paso un escenario de desarrollo simple para demostrar el proceso.

Partimos de la suposición de que ya tienes instalado el entorno de desarrollo Android, incluyendo el Android SDK, Android NDK y el emulador x86 para probar las aplicaciones.  Puedes encontrar recursos con información para hacer esto en http://software.intel.com/es-es/android

Descripción paso a paso de una aplicación sencilla

Supongamos que desde una aplicación Android quisiéramos conocer detalles de bajo nivel sobre el procesador del dispositivo en el que funciona nuestra app. Supongamos también que contamos con un programa que puede determinar esto, pero el problema es que está en C e incluye llamadas a lenguaje ensamblador. El listado 1 muestra el contenido del programa cpuid.c que queremos reutilizar.

Listado 1. Programa legado cpuid.c. Disponible en https://gist.github.com/pedrogk/7427009

(El contenido de ese código es irrelevante para este tutorial, pudo haber consistido en simplemente regresar un arreglo de caracteres con cualquier texto, pero la hicimos de emoción con una llamada a cpuid. Si por cultura general te interesa conocer más sobre cpuid y lo que significan las distintas banderas, consulta http://en.wikipedia.org/wiki/CPUID).

A continuación mostraremos cómo podemos, desde una aplicación Android, llamar la función cpuid_parse() y recibir su resultado.

1. Creación de un proyecto Android predeterminado

Para comenzar, crearemos una carpeta ndk dentro de la que crearemos un nuevo proyecto de Android con la estructura default. Esto se logra por medio del comando “android” el cual ejecutamos de la siguiente forma:

~$ mkdir ndk

~$ cd ndk

~/ndk$ android create project --target android-16 --activity CPUIdApp --package com.example.cpuid --path .

Al hacer esto, la herramienta debe generar un proyecto con la actividad “CPUIdApp” en el paquete com.example.cpuid y usando como referencia el nivel 16 del API de Android (Jelly Bean 4.1). Si exploramos nuestra carpeta, podremos ver que se han creado nuevos archivos y directorios, entre ellos una carpeta src con la estructura com/example/cpuid dentro de la cual está el archivo CPUIdApp.java que es un archivo tipo “Hola Mundo” generado automáticamente.

Una vez creado el proyecto, vamos a compilarlo e instalarlo en un dispositivo o emulador (si no tienes un emulador activado, o no estás usando aceleración por hardware, te recomiendo este artículo donde explico como configurar el emulador con aceleración por hardware).

Para compilar y construir la aplicación, usaremos la herramienta “ant” con la tarea “debug”.

~/ndk$ ant debug

Si la tarea se ejecuta exitosamente, veremos que en el directorio bin tenemos varios archivos, entre ellos CPUIdApp-debug.apk que es nuestra aplicación Android.

Nota: Al momento de crear este artículo, la versión más reciente del Android SDK tiene un bug que provoca que al generar un proyecto default y correr la tarea ant para construirlo, se genera una excepción de tipo java.nio.BufferOverflowException. Si te enfrentas con este problema, usando el SDK Manager baja la versión de los Build-tools a la 18.1.1 (el bug está en la versión 19 de los Build-tools).

El siguiente paso es instalar la aplicación a un dispositivo o emulador. Para ello utilizamos el comando adb, que es parte del Android SDK.

~/ndk$ adb install -r bin/CPUIdApp-debug.apk

Verificando en nuestro dispositivo o emulador, podremos ver que la app se instaló y podemos ejecutarla.

Figura 1. Emulador mostrando nuestra app.

Al hacer clic en la aplicación, puede verse el mensaje predeterminado “Hello World” de la aplicación.

Figura 2. Ejecución de la aplicación default

A continuación modificaremos la aplicación para utilizar código nativo.

2. Invocación de código nativo desde código Java

El listado 2 muestra el código fuente de nuestro archivo /src/com/example/cpuid/CPUIdApp.java que como ya comentamos, fue generado automáticamente al crear el proyecto.

Listado 2. Código Java default. (https://gist.github.com/pedrogk/7426888)

Para utilizar código C/C++ desde nuestro programa de Java, necesitamos hacer la llamada por medio de JNI. Desplegaremos en un TextView el texto que nos regrese la llamada por JNI al código nativo.

El listado 3 contiene la versión ajustada de CPUIdApp.java para realizar esto.

Listado 3. Llamada JNI y despliegue del texto. (https://gist.github.com/pedrogk/7427085)

A continuación utilizaremos la herramienta “javah” para generar los stubs de encabezado para JNI.

3. Utilización de “javah” para generar encabezados stub de JNI para código nativo

Vamos entonces a modificar nuestro código nativo para cumplir con la especificación de llamadas por JNI. La herramienta “javah” puede analizar una o más clases Java y generar los archivos de encabezado correspondientes para C.

Primero necesitamos compilar la versión actualizada de nuestro proyecto así que ejecutamos la tarea ant.

~/ndk$ ant debug

Una vez hecho esto, usamos la herramienta javah para generar el archivo de encabezado para C.

~/ndk$ javah -d jni -classpath bin/classes com.example.cpuid.CPUIdApp

Nota: Si al ejecutar javah obtienes un error similar a “class file for android.app.Activity not found”, es porque requieres indicar el .jar de Android que estás utilizando. Podemos resolver esto indicandolo como un classpath adicional al invocar javah. Si estás utilizando Windows, separa los classpath con ‘;’ y si utilizas Linux hazlo con ‘:’. Ejemplo:  ~/ndk$ javah -d jni -classpath ~/android-sdk/platforms/android-16/android.jar:bin/classes com.example.cpuid.CPUIdApp

Después de ejecutar esto, notaremos que hay una carpeta jni en nuestro proyecto, que contiene el archivo com_example_cpuid_CPUIdApp.h. Este es nuestro archivo de encabezado.

Ahora vamos a crear el archivo de código C correspondiente (“com_example_cpuid_CPUIdApp.c”) al encabezado. El listado 4 muestra el contenido:

Listado 4. Código en C para especificar llamada JNI. (https://gist.github.com/pedrogk/7427437)

Lo que estamos haciendo con este código es llamar la función cpuid_parse de nuestro programa nativo en C enviándole como parámetro un búfer de caracteres en el que queremos que escriba la respuesta. Dicho búfer se regresa como un string de JNI.

4. Generación de código nativo con NDK para x86

Ahora estamos listos para compilar el código nativo utilizando el conjunto de herramientas x86 NDK. Si no conoces NDK o no lo tienes instalado, consulta la guía en http://software.intel.com/es-es/articles/ndk-for-ia  

Para poder compilar nuestro código nativo, vamos a poner nuestro programa cpuid.c (mostrado en el listado 1) en la carpeta “jni” de nuestro proyecto.

Adicionalmente, requerimos crear un archivo "Android.mk" que también va en la carpeta “jni” en el que especificamos cuales son las tareas que debe realizar el NDK, por ejemplo cuales son los archivos a ser compilados, el encabezado y el tipo de compilación (por ejemplo:shared_library).

El listado 5 muestra el contenido de Android.mk para nuestro proyecto. Recuerda que este archivo debe estar dentro de la carpeta “jni” de nuestro proyecto.

Listado 5. Makefile para NDK. (https://gist.github.com/pedrogk/7427531)

Como puedes ver, simplemente estamos indicando cuales son los archivos fuente, y que queremos que se compile como una biblioteca compartida.

Una vez creado el Android.mk y teniendo todos los archivos necesarios en nuestra carpeta jni (Android.mk, com_example_cpuid_CPUIdApp.c, com_example_cpuid_CPUIdApp.h, cpuid.c) podemos regresar al directorio raíz de nuestro proyecto y emitir el comando ndk-build (si la carpeta de NDK no está en tu PATH, debes indicar la ruta completa de donde lo tengas instalado).

~/ndk$ ndk-build APP-ABI=x86

Al usar la opción “APP_ABI=x86” estamos indicando al NDK que queremos generar binarios para arquitectura x86. Si el comando se ejecuta exitosamente, podremos ver que se creó el archivo /ndk/libs/x86/libcpuid.so

Estamos ahora preparados para recompilar nuestra aplicación Android e instalarla o ejecutarla en un emulador x86 o en el dispositivo final.

5. Recompilación, instalación y ejecución de la aplicación Android NDK para AI

Vamos a realizar la siguiente secuencia de comandos para primero limpiar los antiguos archivos, luego recompilar, y luego instalar en nuestro dispositivo o emulador.

~/ndk$ ant debug clean

~/ndk$ ant debug

~/ndk$ adb install -r bin/CPUIdApp-debug.apk

La figura 3 muestra la ejecución de la aplicación mostrando el resultado arrojado por nuestro código nativo.

Figura 3. Corriendo nuestra app accediendo código legado.

Listo. Hemos compilado y ejecutado exitosamente la aplicación Android basada en NDK.

Cómo exportar aplicaciones NDK existentes a dispositivos con arquitectura Intel

Si tienes aplicaciones existentes que utilizan NDK, es muy sencillo modificarlas para que soporten otras arquitecturas, como la de Intel. Simplemente pones tu código nativo bajo la carpeta “jni” tal y como se hizo en este ejercicio, y al ejecutar ndk-build utilizas la opción “APP_ABI := all” para que NDK genere bibliotecas compartidas nativas para todas las plataformas disponibles. El sistema de compilación Android empaquetará automáticamente todas las bibliotecas nativas necesarias dentro de APK y en tiempo de instalación el administrador de paquetes de Android instalará solamente la biblioteca nativa apropiada en base a la arquitectura finalmente usada.

Para exportar una aplicación Android existente con código nativo, usualmente no destinada a x86, el proceso de modificación de la aplicación para hacerla compatible con AI es directo en la mayoría de los casos (como se discute anteriormente), a menos que la aplicación utilice lenguajes o algoritmos de ensamblador de arquitectura específica. Pueden haber otros problemas como la alineación de la memoria o usos de instrucciones específicos de la plataforma. Consulta http://software.intel.com/es-es/articles/ndk-android-application-porting-methodologies para más información.

Resumen

Este artículo mostró como crear y exportar aplicaciones Android basadas en NDK para arquitectura Intel. Recorrimos paso a paso un proceso de creación de una aplicación basada en NDK a ser utilizada en AI, de inicio a fin. Discutimos también el proceso sencillo hecho posible por las herramientas NDK para exportar aplicaciones existentes Android basada en NDK a una AI.