Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ build --worker_sandboxing

build --enable_platform_specific_config

common --repo_env=ACCEPTED_ANDROID_NDK_LICENSE_VERSION=r27c
common --repo_env=ACCEPTED_ANDROID_SDK_LICENSE_VERSION=35

common --android_platforms=@rules_android//:arm64-v8a
common --java_language_version=17
common --java_runtime_version=remotejdk_17
common --tool_java_language_version=17
common --tool_java_runtime_version=remotejdk_17

common:linux --repo_env=CC=clang
build:linux --cxxopt='-std=c++17' --host_cxxopt='-std=c++17'
common:linux --//test:apple_build_tests=False
Expand Down
4 changes: 3 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ use_repo(system_sdk, "system_sdk")
swift = use_extension("//swift:extensions.bzl", "swift", dev_dependency = True)
swift.toolchain(
name = "swift_toolchain",
swift_version = "6.3",
swift_version = "6.3.2",
)
use_repo(
swift,
Expand Down Expand Up @@ -110,6 +110,8 @@ register_toolchains(
dev_dependency = True,
)

include("//examples/cross_compilation/android_app:android.MODULE.bazel")

# Dev dependencies
bazel_dep(name = "bazel_skylib_gazelle_plugin", version = "1.5.0", dev_dependency = True)
bazel_dep(name = "gazelle", version = "0.46.0", dev_dependency = True)
Expand Down
57 changes: 57 additions & 0 deletions doc/standalone_toolchain.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,63 @@ bazel run @rules_swift//tools/swift-releases -- list \
main-snapshot-2024-08-01 --platform xcode --platform ubuntu22.04
```

## Building for Android

The `swift` module extension can be used to download the Swift Android
SDK for writing NDK code in Swift.

Add the `android_sdk` tag, referencing the `toolchain` tag by name (the
Swift module format is not stable across compiler versions, so the SDK
is always downloaded for exactly the toolchain's version):

```bzl
swift.toolchain(
name = "swift_toolchain",
swift_version = "6.3.2",
)

swift.android_sdk(toolchain_name = "swift_toolchain")

# rules_swift provides only the Swift toolchain; register an Android C/C++ cc
# toolchain too. For example, @androidndk//:all from hermetic_android_toolchains:
android = use_extension("@hermetic_android_toolchains//:extensions.bzl", "android")
android.sdk(version = "35", build_tools_version = "35.0.0")
android.ndk(version = "r27c", api_level = 28)
use_repo(android, "androidndk")

register_toolchains(
"@swift_toolchain//:swift_toolchain_android_aarch64_xcode",
"@swift_toolchain//:swift_toolchain_android_x86_64_xcode",
"@androidndk//:all",
)
```

See `examples/cross_compilation/android_app` for an end to end example.

### JNI shared libraries

To produce a JNI library (`lib<name>.so`) loadable with
`System.loadLibrary`, Use something like:

```bzl
swift_binary(
name = "<name>",
linkshared = True,
...
)
```

Export JNI functions with `@_cdecl`:

```swift
import Android

@_cdecl("Java_com_example_swiftjni_NativeBridge_greetingFromSwift")
public func greetingFromSwift(_ env: UnsafeMutablePointer<JNIEnv?>, _ clazz: jclass) -> jstring? {
```

See `examples/cross_compilation/android_app` for an end to end example.

## Using the extension from a non-root module

The extension is intended for the root module — it fails if a non-root
Expand Down
19 changes: 19 additions & 0 deletions examples/cross_compilation/android_app/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.swiftjni">

<uses-sdk
android:minSdkVersion="28"
android:targetSdkVersion="34" />

<application android:label="Swift JNI Example">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
71 changes: 71 additions & 0 deletions examples/cross_compilation/android_app/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
load("@bazel_skylib//rules:build_test.bzl", "build_test")
load("@rules_android//rules:rules.bzl", "android_binary")
load("@rules_cc//cc:defs.bzl", "cc_library")
load("@rules_kotlin//kotlin:android.bzl", "kt_android_library")
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
load("//swift:swift.bzl", "swift_binary", "swift_library")

# bazel run //examples/cross_compilation/android_app:run
sh_binary(
name = "run",
srcs = ["run.sh"],
args = [
"$(rlocationpath :app.apk)",
"$(rlocationpath @androidsdk//:adb)",
"$(rlocationpath @android_emulator//:emulator/emulator)",
"$(rlocationpath @android_system_image//:system.img)",
],
data = [
":app.apk",
"@android_emulator//:all",
"@android_emulator//:emulator/emulator",
"@android_system_image//:all",
"@android_system_image//:system.img",
"@androidsdk//:adb",
],
)

android_binary(
name = "app",
manifest = "AndroidManifest.xml",
deps = [
":app_lib",
":swift_jni",
],
)

kt_android_library(
name = "app_lib",
srcs = [
"java/com/example/swiftjni/MainActivity.kt",
"java/com/example/swiftjni/NativeBridge.kt",
],
custom_package = "com.example.swiftjni",
manifest = "AndroidManifest.xml",
)

build_test(
name = "app_build_test",
targets = [":app"],
)

# https://github.com/bazelbuild/rules_android/pull/531
cc_library(
name = "swift_jni",
srcs = [":SwiftJNI"],
target_compatible_with = ["@platforms//os:android"],
)

swift_binary(
name = "SwiftJNI",
srcs = ["Sources/SwiftJNI/SwiftJNI.swift"],
linkshared = True,
target_compatible_with = ["@platforms//os:android"],
deps = [":Greeter"],
)

swift_library(
name = "Greeter",
srcs = ["Sources/Greeter/Greeter.swift"],
module_name = "Greeter",
)
21 changes: 21 additions & 0 deletions examples/cross_compilation/android_app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Building Swift for Android

A complete, runnable Android app that cross-compiles Swift to Android
and runs it through the JNI. See `doc/standalone_toolchain.md` for the
toolchain setup.

## Build & run

Hermetically:

```sh
bazel run //examples/cross_compilation/android_app:run
```

With a locally installed Android SDK and device:

```sh
bazel build //examples/cross_compilation/android_app:app
adb install -r bazel-bin/examples/cross_compilation/android_app/app.apk
adb shell am start -n com.example.swiftjni/.MainActivity
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
public struct Greeter {
private let subject: String

public init(subject: String) {
self.subject = subject
}

public func greeting() -> String {
return "Hello from Swift, \(subject)!"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Android
import Greeter

// The JNI entry point, written entirely in Swift: `import Android` provides the
// JNI types, and `@_cdecl` gives the function the `Java_<package>_<class>_<method>`
// symbol name that `NativeBridge.greetingFromSwift()` binds to.
@_cdecl("Java_com_example_swiftjni_NativeBridge_greetingFromSwift")
public func greetingFromSwift(_ env: UnsafeMutablePointer<JNIEnv?>, _ clazz: jclass) -> jstring? {
let message = Greeter(subject: "Android").greeting()
return message.withCString { cString in
env.pointee!.pointee.NewStringUTF(env, cString)
}
}
115 changes: 115 additions & 0 deletions examples/cross_compilation/android_app/android.MODULE.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
bazel_dep(name = "hermetic_android_toolchains", version = "0.1.1", dev_dependency = True)
bazel_dep(name = "rules_android", version = "0.7.3", dev_dependency = True)
bazel_dep(name = "rules_android_ndk", version = "0.1.5", dev_dependency = True)
bazel_dep(name = "rules_java", version = "9.3.0", dev_dependency = True)
bazel_dep(name = "rules_jvm_external", version = "7.0", dev_dependency = True)
bazel_dep(name = "rules_kotlin", version = "2.2.2", dev_dependency = True)

swift = use_extension("//swift:extensions.bzl", "swift", dev_dependency = True)
swift.android_sdk(toolchain_name = "swift_toolchain")

rules_java_toolchains = use_extension(
"@rules_java//java:extensions.bzl",
"toolchains",
dev_dependency = True,
)
use_repo(rules_java_toolchains, "remote_java_tools")

# rules_android's own Maven dependencies, pinned to a checked-in lock file:
# REPIN=1 bazel run @rules_android_maven//:pin
# https://github.com/bazelbuild/rules_android/issues/485
maven = use_extension(
"@rules_jvm_external//:extensions.bzl",
"maven",
dev_dependency = True,
)
maven.install(
name = "rules_android_maven",
aar_import_bzl_label = "@rules_android//rules:rules.bzl",
artifacts = [],
known_contributing_modules = ["rules_android"],
lock_file = "//examples/cross_compilation/android_app:rules_android_maven_install.json",
repositories = [
"https://maven.google.com",
"https://repo1.maven.org/maven2",
],
use_starlark_android_rules = True,
)
use_repo(maven, "rules_android_maven")

android = use_extension(
"@hermetic_android_toolchains//:extensions.bzl",
"android",
dev_dependency = True,
)
android.sdk(
build_tools_version = "35.0.0",
version = "35",
)
android.ndk(
api_level = 28,
version = "r27c",
)
use_repo(android, "androidndk", "androidsdk")

# Make @rules_android's @androidsdk labels resolve to the hermetic SDK.
rules_android_sdk = use_extension("@rules_android//rules/android_sdk_repository:rule.bzl", "android_sdk_repository_extension", dev_dependency = True)

override_repo(rules_android_sdk, "androidsdk")

# Make @rules_android_ndk's @androidndk labels resolve to the hermetic SDK.
rules_android_ndk = use_extension("@rules_android_ndk//:extension.bzl", "android_ndk_repository_extension", dev_dependency = True)

override_repo(rules_android_ndk, "androidndk")

register_toolchains(
"@androidndk//:all",
"@androidsdk//:all",
dev_dependency = True,
)

register_toolchains(
"@androidndk//:all",
dev_dependency = True,
)

http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
name = "android_emulator",
build_file_content = """\
package(default_visibility = ["//visibility:public"])

exports_files(["emulator/emulator"])

filegroup(name = "all", srcs = glob(["emulator/**"]))
""",
dev_dependency = True,
sha256 = "25378c67fd5bd03178e3a478b866496da8545e969df3a7a26ce9167772ffc026",
url = "https://dl.google.com/android/repository/emulator-darwin_aarch64-15565763.zip",
)

http_archive(
name = "android_system_image",
build_file_content = """\
package(default_visibility = ["//visibility:public"])

exports_files(["system.img"])

filegroup(name = "all", srcs = glob(["**"]))
""",
dev_dependency = True,
sha256 = "1447958a4c6747c44390ac5f5f4c894be6d1dfce93868a0385a95c5f0ae4c339",
strip_prefix = "arm64-v8a",
url = "https://dl.google.com/android/repository/sys-img/android/arm64-v8a-34_r04.zip",
)

register_toolchains(
"@swift_toolchain//:swift_toolchain_android_aarch64_ubuntu22.04",
"@swift_toolchain//:swift_toolchain_android_aarch64_ubuntu22.04-aarch64",
"@swift_toolchain//:swift_toolchain_android_aarch64_xcode",
"@swift_toolchain//:swift_toolchain_android_x86_64_ubuntu22.04",
"@swift_toolchain//:swift_toolchain_android_x86_64_ubuntu22.04-aarch64",
"@swift_toolchain//:swift_toolchain_android_x86_64_xcode",
dev_dependency = True,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.swiftjni

import android.app.Activity
import android.os.Bundle
import android.widget.TextView

/** Displays the greeting computed by Swift, reached via JNI. */
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val textView = TextView(this)
textView.text = NativeBridge.greetingFromSwift()
setContentView(textView)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.swiftjni

/**
* Loads the Swift JNI shared library (`libSwiftJNI.so`, built by the `:SwiftJNI`
* `swift_binary(linkshared = True)`) and exposes its Swift entry point to Kotlin.
*
* The native method binds by name to the Swift `@_cdecl` function
* `Java_com_example_swiftjni_NativeBridge_greetingFromSwift`, which in turn
* calls the `Greeter` `swift_library` — completing the
* Kotlin -> Swift (in the `.so`) -> Swift library call chain.
*/
object NativeBridge {
init {
System.loadLibrary("SwiftJNI")
}

external fun greetingFromSwift(): String
}
Loading
Loading