Bootstrap repo for ECL on android
This repo demonstrates how to build an Android app that uses ECL Common Lisp code.
Thanks to the following projects for inspiraition and guidance:
-
ECL itself, specifically the
android
example -
EQL and EQL5-Android, most especially the examples
REPL
andmy
in EQL5-Android, as well as the code to cross-compile for Android in theutils
directory
Some code has been copied/adapted from EQL
and EQL5-Android
.
The simple way is to build the docker container and use it to cross-compile and build your Android app:
export USER=<your user nick> # optional and maybe not needed, check: env | grep USER
./build.sh
Now (after a quite lengthy build process) you should have a docker
container named android-ecl-bootstrap
, which you can start by doing:
export CODE_DIR=<your fully qualified code location> # optional
./start.sh
This script assumes your code lives in $HOME/code
. If that is not
the case, export the correct location as described above.
You should be able to build the example now, by doing:
cd code/example
./2-build-and-install.sh
On the first run it'll download and install a bunch of stuff, which is
saved in your mounted home directory (see start.sh
; by default this
is at $HOME/docker.state/android-ecl-bootstrap
on your host
machine). You can remove this line, if you want, but then each time
you start the docker container it will be blank and redownload
everything.
The first run can take a couple of minutes to finish.
If there were no errors, you should find an apk at
app/build/outputs/apk/debug/app-debug.apk
If you pass install
as an argument to 2-build-and-install.sh
, it
will attempt to install the app on your connected phone. For this to
work, ideally you should setup your network to provide your phone with
a fixed IP address and change ANDROID_IP
in start.sh
to reflect
that IP.
Alternatively, you can always manually connect your mobile test device
through adb
before running ./2-build-and-install.sh install
. Do
something like this (in the running docker container, obviously):
adb connect <mobile device IP>:<port>
If you don't want to use docker (why?) you will need to setup your build environment yourself. You're on your own there, but if you basically perform all the actions defined in the Dockerfile (adapted to correct paths, etc), you should be able to get things up and running.
This repo serves as a proof of concept for building Android apps with
Common Lisp using ECL. In the example
directory you'll find
everything to build the example app.
It should be fairly straightforward to adapt it and use it as a base to build your own app.
I suggest the following procedure:
-
Make a copy of
example
to your code directory (seeCODE_DIR
env). -
Put your new copy into some source code control system.
-
Replace all mentions of
org.example.testapp
with your own app name. If your app iscom.fancydomain.clftw
, do:
find . -path ./.git -prune -o -type f -exec sed -i 's/org_example_testapp/com_fancydomain_clftw/g' '{}' \;
find . -path ./.git -prune -o -type f -exec sed -i 's/org.example.testapp/com.fancydomain.clftw/g' '{}' \;
find . -path ./.git -prune -o -type f -exec sed -i 's/testapp/clftw/g' '{}' \;
You might also want to grep for TAG
and replace that with your own
tag so you can find the log messages your app sends to logcat easily.
- You should now be able to build (and install) your app:
./start.sh # unless you're already inside the docker container
cd ~/code/clftw
./2-build-and-install.sh install
If you look in example/app/src/main/java/org/example/testapp/
you'll
find two kotlin
source files: MainActivity.kt
and
EmbeddedCommonLisp.kt
.
MainActivity.kt
is where your app starts. This is just a bare
template adapted from the default that Android Studio gives you if you
select a Native C++
project to start.
MainActivity.kt
uses the other file (EmbeddedCommonLisp.kt
),
creating a private ECL
instance, which it first calls initialize
on, then start
.
In initialize
we copy all the .fas
, .asd
and .lisp
files we
added as assets to the app to a place where we can access them later
on.
Next the call to ECL.start
calls the JNI function JNIstart
defined
in example/app/src/main/cpp/src/cxx/main.cpp
. In that file you'll
see that the function is actually called
Java_org_example_testapp_EmbeddedCommonLisp_JNIstart
which is JNI's
way to automatically discover native functions. You should also take
note of the matching external fun
declaration in
EmbeddedCommonLisp.kt
.
This function does just the bare minimum, logging some stuff and then
calling ecl_boot
, defined in ecl_boot.cpp
in the same directory.
The function ecl_boot
sets up our Lisp environment and shows some
examples how to log to the android log from lisp, how to load other
parts of the ECL system (see the // load asdf
part and the calls
where we require
sb-bsd-sockets
) and so on.
Then it loads all the systems we added as assets by loading the file
load.lisp
, which was created and added during the app build, see
Makefile
in example/app/src/main/lisp/module
and the supporting
files in example/app/src/main/lisp/utils/
.
In example/app/src/main/lisp/module
you'll find complete examples of
how to build the Lisp code for your project. In principle the only
files you'd need to change are the .asd
, the .deps
and the .lisp
files.
You can also symlink only the .asd
file of your project into this
directory. It works mostly the same as the local-projects
directory
in a Quicklisp
system, with some added requirements, for which, read
on.
The separate package files are of course not strictly necessary.
The example as set up actually builds two separate .fas
files
(module.fas
and other.fas
), although only one is actually used in
the rest of the example project code.
If you add the three required files (.asd
, .deps
, and .lisp
(and
however many more .lisp
files you need, of course)) for another
module, the Makefile will automatically pick it up and build the
.fas
for you when you run ./2-build-and-install.sh
, and copy it to
the correct place for inclusion in the assets
of your app.
Be aware, though, that all built .fas
files will be included in
your app's apk
, so don't put stuff here you don't want included.
The .deps
files should quickload
all the required dependencies of
your module. Basically, every :depends-on
entry in your .asd
file
should have a corresponding (ql:quickload ..)
line in the .deps
file of your project. The .asd
and .deps
file should have the same
base filename (and that should be the same as your defsystem
name
for Quicklisp/asdf to work). So the system :module
in the example
has the files module.asd
and module.deps
.
The way the example is set up, dependencies will be combined with the
requiring module in one .fas
file. Sometimes this results in very
large .fas
files, and there is some evidence that this could cause
problems with loading them in ECL on Android.
The alternative is to separately add all the .fas
files and load
them. To do this, set FAS_TYPE
at the top of the Makefile
at
example/app/src/main/lisp/module/Makefile
to separate
instead of
combined
.
Then, instead of symlinking your .asd
file and creating a .deps
file for the system you want to add, symlink all the .asd
files of
dependencies into example/app/src/main/lisp/module/
.
For this to work, you need to make sure that all dependencies are added, which includes all dependencies of the dependencies!
Now, when you build you app, you should see all the separate .fas
files created and added to the directory
example/app/src/main/lisp/fas/
, as well as a load.lisp
file in
that location. The load.lisp
file should handle all the dependencies
in the correct order.