Translate

Sunday, March 25, 2012

Development on Android NDK using VIM and CGDB


Android representing Vim



Using Vim to program Native Android code (NDK).


You've likely found this page since you're like me, a vim editor advocate that loves the productive features of vim and are getting into Android programming, but maybe you aren't quite sure how to get started.  I don't really find much benefit in using IDE's for C/C++ game development since many times I am making custom make targets to create assets or other such customization that are painful or not possible in IDEs.  Also, let's face it, vim and emacs (while haters call old and dated) have features that aren't accessible in other editors at all or without buggy plugins etc.

Here I will document my findings and how I am currently using vim / ctags /cgdb and the typical jdk to develop an Android game.  I will assume you have both the latest Android SDK and NDK installed and your environment is working correctly (also you will probably want the Android source code so you can view SDK definitions).  I also assume you're on a Linux/Unix system, however with Cygwin, this should all work fine also.


Firstly how do you get all those nifty features that IDEs have for jumping around to definitions?  


That's simply a mater of getting your Ctags setup correctly (and the path(s) vim looks for tags).  Please, if you haven't already, get the latest ctags (source/package etc) and install using your favorite method.

You will likely want the tags for the entire SDK and NDK available, since, let's face it, you'll likely be coding parts in Java and C/C++ due to the way Google has setup the Android system.

Since I am partial to Makefiles due to how customizable they are I will show you how I do all this in a Makefile.  Take care that you will only want to recreate the tags when you update the SDK or NDK, not every time you build (unless you like waiting forever, then be my guest).  The other exception is, of course, if you are doing something weird and actually editing the android source code.


I do all these steps by creating a "Makefile" at the root of my project folder, with all these custom targets.  This root Makefile calls ndk-build, ant build and all the regular Android build commands, to save you time and typing (more on that later).

Here is the make target I use for creating SDK/NDK tags:

sdk_tags:
  -ctags -R -o $(HOME)/.vim/ctags/android_java --sort=yes \
    --fields=+iaKsS --extra=+q \
    --c++-kinds=+p \
    $(ANDROID_SDK)/android_src_git/

You would just call this once every time you install a new Androud NDK or SDK.  Notice the directory "android_src_git", this is just where I downloaded the complete android source code to use for my tags (beware it is pretty huge).  It is not downloaded as part of the normal SDK, so be warned you need to use git version control tool to get this.

So in reality when you get a new SDK, just go into that directory with your console and run git pull.  They will both be relatively in sync, depending on what SDK you're building against.

I tie this all into vim by creating a openproj bash script to setup my environment:

 1 #!/bin/bash
 2 # CODE_TAGS is set up in vimrc file as:
 3 #    set tags+=$CODE_TAGS
 4
 5 export "CODE_TAGS=$HOME/.vim/ctags/android_java";  \
 6     vim  src/com/example/Cool/Game/*.java \
 7       jni/*.cpp \
 8       jni/*.h \
 9       jni/Box2D/*/*.cpp

You can save this in the top level if your project directory, and make sure has the execute permission set.   

Notice I set the CODE_TAGS environment variable at run time, not in my bashrc file etc.  This is due to me replicating this technique on many different types of projects from, Gameboy Advance to PSP.

Ok, now we will also want to build new tags each time we change java or C++ code, so we can accomplish that by creating a target based on the tags file, and put all Java and C++ files as this target's dependency.  Then, each time any JAVA or C++ file changes the tags file will be remade by ctags:

Some where near the top of your root Makefile:



17 JAVA_FILES := $(wildcard src/*/*/*.java) $(wildcard src/*/*/*/*.java)
18 CPP_FILES := \
19   $(wildcard jni/*.cpp) \
20   $(wildcard jni/*.h) \
21   $(wildcard jni/*/*.cpp) \
22   $(wildcard **/.cpp) \


Obviously you are going to want to change those paths to where you are keeping cpp and java files. For example, on line 21, notice that two stars, well, if you have cpp files nested 2 levels deep, put in $(wildcard jni/*/*/*.cpp) \

Place this, somewhere after these definitions (but NOT the first target in your makefile, that will be the default target, which I like to use for creating the debug apk file):


68 tags: $(JAVA_FILES) $(CPP_FILES)
69   -ctags -R --sort=yes \
70   --java-types=cfimp \
71     --fields=+iaKsS --extra=+q \
72     --c++-kinds=+p \
73     ./src/ ./


Notice tags is an actual file, not a .PHONY target, so it checks to time stamp of that file and compares against all your source files in $(JAVA_FILES) and $(CPP_FILES) if tags is older than any of those, it is recreated.

Ok, let's use what we have learned and see a working example.

Here's what these new targets will look like in a working Makefile:



 1 PACKAGE_NAME = com.example.Cool.Game
 2 ACTIVITY_NAME = .Physics2d
 3 
 4
 5 DEBUG_TARGET= bin/Physics2d-debug.apk
 6 RELEASE_TARGET= bin/Physics2d.apk
 7
 8 .PHONY: force
 9
10 ANT_FLAGS = -emacs
11 NDK_DEBUG_FLAGS = \
12                   "NDK_DEBUG=1" \
13                   "V=1" \
14
15 NDK_RELEASE_FLAGS =
16
17 JAVA_FILES := $(wildcard src/*/*/*.java) $(wildcard src/*/*/*/*.java)
18 CPP_FILES := \
19   $(wildcard jni/*.cpp) \
20   $(wildcard jni/*.h) \
21   $(wildcard jni/*/*.cpp) \
22   $(wildcard **/.cpp) \
23
24 all: $(DEBUG_TARGET)
25
26 $(DEBUG_TARGET): force
27   $(MAKE) tags
28   ndk-build $(NDK_DEBUG_FLAGS) 
29   ant debug
30
31 $(RELEASE_TARGET): force
32   $(MAKE) tags
33   ndk-build $(NDK_RELEASE_FLAGS) 
34   ant release
35
36 run: install
37
38 install:
39   ant clean
40   ndk-build $(NDK_DEBUG_FLAGS) 
41   ant $(ANT_FLAGS) debug install
42   adb shell "am start -n $(PACKAGE_NAME)/$(ACTIVITY_NAME)"
43
44 install_release:
45   ant clean
46   ndk-build $(NDK_RELEASE_FLAGS) 
47   ant $(ANT_FLAGS) release install
48   adb shell "am start -n $(PACKAGE_NAME)/$(ACTIVITY_NAME)"
49
50 
51
52 uninstall:
53   ndk-build uninstall
54
55 clean: force
56   ant clean
57   ndk-build $(NDK_DEBUG_FLAGS) clean
58
59 sdk_tags:
60   -ctags -R -o $(HOME)/.vim/ctags/android_java --sort=yes \
61     --fields=+iaKsS --extra=+q \
62     --c++-kinds=+p \
63        \
64     $(ANDROID_SDK)/android_src_git/
65
66 #--java-types=cfimp --language-force=java \
67
68 tags: $(JAVA_FILES) $(CPP_FILES)
69   -ctags -R --sort=yes \
70   --java-types=cfimp \
71     --fields=+iaKsS --extra=+q \
72     --c++-kinds=+p \
73     ./src/ ./
74
75
76 


As your attorney, I advise you to...


My codes changed and the program's the same... am I CrAZY??!!!!




Ok, I know you're going to ask about this part:

38 install:
39   ant clean




Why am I cleaning before installing? Waste of time right?  Well version 16 of the Android SDK had a bug in which a new apk file would NEVER be recreated when java code changed (and a previous apk file already existed there).  Therefore you'd change something and the apk file would never change when installing, sending you to bat country wondering if you were crazy or if there was a reason your program was not changing.

It has been reported as fixed in version 17 which was just released. But I haven't verified this.

What Your Workspace should look like

So your Android project directory should look something like this (bold files we've created):


  • AndroidManifest.xml      
  • build.xml
  • libs  
  • obj
  • project.properties  
  • src    
  • assets  
  • gen
  • local.properties  
  • openproj
  • res    
  • tags
  • bin     
  • jni
  • Makefile  
  •  proguard.cfg


Now, provided you have all the environment variables set up for SDK/NDK etc,  if you type "make" you should see the the JNI native code being built in the "ndk-build" step (debug mode), all Java glue code and the debug version of the APK being built.

When you are ready to do some coding just run ./openproj, and provided you set tags+=$CODE_TAGS  in your ~/.vimrc file and all the tag paths correct etc, you should have a sweet vim Android development setup. 



In my next blog entry, I will talk about using CGDB and issues wrestling with OpenGLES context and how we get nutty and link C++/C with Java with JNI!





Here's a running log of Issues ran into in general Android development so far that will be addressed as I have answers:
  • Debug symbols for dwarf are broken in latest NDK (r7)
    • Will say: "gdb.setup:4: Error in sourced command file" and when placing breakpoints in gdb that there is no such line on that file (even though there clearly is).
    • Solution: Needed to add -gstabs+ to LOCAL_CFLAGS in all Android.mk makefiles.
  • Some rendering works in emulator but not on hardware
    • All triangles are not rendered (textured triangle should be fine, but unlit, colored non-textured don't show up in hardware).
    • glColor4f has no effect on hardware (EVO 3D), but works in emulator.
    • Solution: There was something the EVO 3D didn't like about my orthographic projection.  I projected the to the entire pixel space (540x960), and the hardware didn't like that for some reason. Scaled back to screen space projection of (actual_w/actual_height)*40 X 40) and that fixed the rendering issue. 
    • I used: glOrthof(0, aspect*40.0f, 0, 40.0f, -1.0f, 1.0f);
  • ndk-gdb "package not found" error
  • from ndk-gdb :5039: Connection refused.
    • Solution?: Got this error when an emulator process was hanging on with gdb and not being killed.
  • Appears you cannot use integer arrays in strings.xml for your application preferences
  • Not Android related: Not all java errors end up in error file for vim (cn cN error navigation).  But it seems all C++ and errors work fine.  Since most of my dev work will be in C++ I don't care much, but I know you can change how vim parses errors in the vimrc file, but I haven't put in the time to merge the C++/C error regex with the javac error parsing.