Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AArch64 Android support #13065

Merged

Conversation

HertzDevil
Copy link
Contributor

@HertzDevil HertzDevil commented Feb 11, 2023

Adds bindings for Android's Bionic C runtime library for AArch64 targets. This is a more polished version of this branch, with all the lib funs manually verified against the Android headers. This allows building standard ELF executables that run on an Android shell.

Compiler and standard library changes

  • {{ flag?(:android) }} returns true if the target system is Android. The directory name under lib_c is now <architecture>-android.
  • The LLVM target triple for AArch64 Android is normalized to remove the Android API level.
  • The Android API level is defined within Crystal code as LibC::ANDROID_API (see below).
  • System::User#name is unavailable on Bionic, so that field now defaults to #username.

Android API level

The target Android API level, accessible via LibC::ANDROID_API, can be configured by either the ANDROID_PLATFORM or the ANDROID_NATIVE_API_LEVEL environment variable (the names come from the official CMake toolchain file), and should be one of the values from this list, optionally preceded by android-. If neither environment variable is defined, the default API level is 31 (Android 12 / S). This is a fairly recent, but appropriate value, as all newly published Android apps must target this level too.

The constant is defined in src/lib_c.cr, corresponding to __ANDROID_API__ being a built-in #define with the Android toolchains. Bionic depends on this API level, and so does the Android NDK. The actual minimum version fully supported in this PR is 28; below this level, certain C functions are defined inline in the Bionic headers rather than exported, and this PR does not recreate them in Crystal.

Simple cross-compilation

After rebuilding Crystal itself on a host with this PR applied, the following test program should demonstrate that cross-compilation is indeed working:

require "lib_c"

lib LibC
  fun printf(fmt : Char*, ...) : Int
end

class String
  def to_unsafe
    pointerof(@c)
  end
end

# this shows the host configuration
LibC.printf("Hello world from %s\n", Crystal::DESCRIPTION)

Compile with:

bin/crystal build --cross-compile --target=aarch64-linux-android --prelude=empty test.cr
"$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$HOST_TAG/bin/clang" "--target=aarch64-linux-android$API_LEVEL" test.o

where $ANDROID_NDK_ROOT points to the root directory of your Android NDK installation, $HOST_TAG would be one of the values listed here according to your host architecture, and $API_LEVEL should match the declared API level in Crystal. The resulting a.out should then run successfully under an ADB shell with an output like:

Hello world from Crystal 1.8.0-dev [14663b6e7] (2023-02-11)

LLVM: 14.0.1
Default target: aarch64-apple-darwin22.2.0

Note
On an ADB shell, files can be placed under /data/local/tmp for execution permission, even on unrooted devices. Any other directory will most likely result in a "permission denied" error. On Termux, which is described below, any executable within Termux's home directory should have no issues executing.

Default prelude

Cross-compiling using the default prelude is a bit more involved because all the minimum runtime dependencies must also be cross-compiled. The following script builds Boehm GC, PCRE2, and libevent for Android:

#!/bin/sh

set -eux

HOST=darwin-x86_64
ANDROID_API_LEVEL=31

ANDROID_ABI=arm64-v8a
TOOLCHAIN_DIR=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/${HOST}
TARGET_TRIPLE=aarch64-linux-android${ANDROID_API_LEVEL}
CC=${TOOLCHAIN_DIR}/bin/clang
CMAKE_ARGS="-DCMAKE_TOOLCHAIN_FILE=${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake -DANDROID_ABI=${ANDROID_ABI} -DANDROID_PLATFORM=android-${ANDROID_API_LEVEL}"

mkdir static
mkdir shared

git clone -b v8.2.2 https://github.com/ivmai/bdwgc.git bdwgc
git clone -b v7.6.14 https://github.com/ivmai/libatomic_ops.git bdwgc/libatomic_ops
pushd bdwgc
mkdir so
pushd so
cmake .. $CMAKE_ARGS
cmake --build . --config Release
popd
cmake . $CMAKE_ARGS -DBUILD_SHARED_LIBS=OFF
cmake --build . --config Release
popd
cp bdwgc/libgc.a static/
cp bdwgc/so/libgc.so shared/
rm -rf bdwgc

git clone -b pcre2-10.42 https://github.com/PCRE2Project/pcre2.git pcre2
pushd pcre2
cmake . $CMAKE_ARGS -DBUILD_SHARED_LIBS=ON -DPCRE2_BUILD_PCRE2GREP=OFF -DPCRE2_BUILD_TESTS=OFF -DPCRE2_SUPPORT_UNICODE=ON -DPCRE2_SUPPORT_JIT=ON
cmake --build . --config Release
popd
cp pcre2/libpcre2-8.a static/
cp pcre2/libpcre2-8.so shared/
rm -rf pcre2

git clone https://github.com/libevent/libevent.git libevent
pushd libevent
git checkout 8f47d8de281b877450474734594fdc0a60ee35d1
cmake . $CMAKE_ARGS -DEVENT__LIBRARY_TYPE=BOTH -DEVENT__DISABLE_OPENSSL=ON -DEVENT__DISABLE_MBEDTLS=ON -DEVENT__DISABLE_BENCHMARK=ON -DEVENT__DISABLE_TESTS=ON -DEVENT__DISABLE_REGRESS=ON -DEVENT__DISABLE_SAMPLES=ON
cmake --build . --config Release
popd
cp libevent/lib/libevent.a static/
cp libevent/lib/libevent-2.2.so shared/libevent.so
rm -rf libevent

This time we will build a more "complex" program:

puts "Hello world from #{Crystal::DESCRIPTION.match(/Default target: (.*)/).not_nil![1]}"

Compile a statically linked binary with:

bin/crystal build --cross-compile --target=aarch64-linux-android -Duse_pcre2 test.cr
"$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$HOST_TAG/bin/clang" "--target=aarch64-linux-android$API_LEVEL" "-L.../static" -lm -lgc -levent -lpcre2-8 test.o

The output is just Hello world from aarch64-apple-darwin22.2.0 this time.

A dynamically linked library can be obtained by using "-L.../dynamic" instead. The .so files themselves should also be copied to somewhere on the device; since the system library directories are protected, they are ideally accessed via LD_LIBRARY_PATH. (Binaries built within Termux have a RUNPATH that refers to Termux's own library directory, but the same applies.)

Running Crystal on Termux

Note
Termux's official package repositories only provide the most recent LLVM version, which means you will need #13173 for LLVM 15 support.

The Crystal compiler requires a linker, which is not straightforward to set up in a vanilla Android environment; Termux is a preferred choice for setting up a development environment that still uses the Android NDK. First run bin/crystal build --cross-compile --target=aarch64-linux-android --release src/compiler/crystal.cr on the host system (this will include playground and interpreter support). Then on Termux run:

pkg install git clang libgc openssl make
# simply clone the official repository instead if this PR is merged
git clone -b feature/aarch64-android https://github.com/HertzDevil/crystal
cd crystal
mkdir .build
cc -c -o llvm_ext.o src/llvm/ext/llvm_ext.cc $(llvm-config --cxxflags)
cc -o .build/crystal crystal.o llvm_ext.o $(llvm-config --libs --system-libs --ldflags) -lstdc++ -lm -lgc -levent -lssl -lcrypto -lffi -lz -lpcre

From here the bin/crystal wrapper script will pick up the newly built compiler, and make install PREFIX=... will produce a portable Crystal installation which can be subsequently used to rebuild Crystal itself on Termux.

A handful of specs are disabled on Android because they are difficult to test on an unrooted device. Additionally, Bionic's iconv supports only UTF-8/16/32 and breaks on null characters. With those limitations in mind, make std_spec compiler_spec primitives_spec FLAGS=-Duse_libiconv threads=1 is confirmed to pass on Termux.

Just give me something that runs on an Android

Then you don't need this PR - Termux's PRoot Distro will be good enough, although that doesn't bind to Bionic. The main goal of this PR is to enable Android app development down the road, getting the compiler to run on Termux is just a bonus.

References

Copy link
Member

@straight-shoota straight-shoota left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. 👍

We should add this target to the smoke tests GHA.

I'd prefer to extract the change to System::User#name implementation to a separate PR. It's a requirement to support bionic, but it's a general change not directly related to android.

Maybe also a refactor of the StaticFileHandler time frame.

spec/std/http/server/handlers/static_file_handler_spec.cr Outdated Show resolved Hide resolved
@straight-shoota straight-shoota added this to the 1.8.0 milestone Mar 6, 2023
Copy link
Member

@beta-ziliani beta-ziliani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make sense to add a full test (even if at nightlies). Most of the specs run IIUC.

@@ -1,4 +1,4 @@
{% skip_file if flag?(:win32) %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this spec being run today in some ararch64 platforms? Why exclude it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is by sheer coincidence that it doesn't break on some AArch64 targets. To this date I don't think the situation has ever improved

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean because of LLVM, or something Crystal specific? In the first case, maybe it got fixed (I couldn't find any open bug there)? In the second case, we should be tracking it. It doesn't work there in your M2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could implement the whole VaList thing ourselves if we want to, similar to Rust (#9422 (comment)); until then we should not assume that VaList is supported on any AArch64 targets at all. (This refers to the whole VaList type and not just the @[Primitive(:va_arg)] part.)

@HertzDevil
Copy link
Contributor Author

If by full test you mean a separate CI workflow (Android emulator runners do exist), I don't know if it is really worth the effort.

@straight-shoota
Copy link
Member

Adding CI infra is out of scope for this PR. This is just platform support.

@beta-ziliani
Copy link
Member

beta-ziliani commented Mar 7, 2023

If by full test you mean a separate CI workflow (Android emulator runners do exist), I don't know if it is really worth the effort.

👍

Adding CI infra is out of scope for this PR. This is just platform support.

Sure. It was more of an exploratory question about how supported this platform could be.

@straight-shoota straight-shoota merged commit 374224d into crystal-lang:master Mar 9, 2023
@HertzDevil HertzDevil deleted the feature/aarch64-android branch March 9, 2023 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants