dioxus icon indicating copy to clipboard operation
dioxus copied to clipboard

Permission, Building Blocks for Native APIs

Open wheregmis opened this issue 2 months ago • 18 comments

This PR introduces a comprehensive linker-based permission management for automatic update of manifest files and android native plugin building foundation for Dioxus. The implementation is inspired by Manganis's linker-based asset collection approach.

Build on top of #4932 so diff is huge for now

Two main motivation for this PR

  1. To automatically handle the manifest file and info.plist file instead of invoking, ejecting or providing custom file.
  2. Able to pass in the java file path through linker, copy and bundle them (Opening doors for android native apis development)

Currently packages/geolocation and geolocation example resides within this PR, its just for the ease of testing, and will not be upstreamed

Here is a breakdown of how things are working for now.

Android (Kotlin / Gradle)

Source layout

  • Kotlin sources + Gradle wrapper live under packages/geolocation/android/
  • The module builds a release .aar: android/build/outputs/aar/geolocation-plugin-release.aar

build.rs responsibilities

packages/geolocation/build.rs builds the Gradle module whenever the target triple contains android:

  1. Respects the toolchain env exported by the CLI (DX_ANDROID_JAVA_HOME, DX_ANDROID_SDK_ROOT, DX_ANDROID_NDK_HOME, DX_ANDROID_ARTIFACT_DIR). If those are absent (non-dx builds) it falls back to ANDROID_* or JAVA_HOME.
  2. Runs ./gradlew assembleRelease inside packages/geolocation/android.
  3. Copies the resulting .aar into the CLI-provided artifact staging dir ($DX_ANDROID_ARTIFACT_DIR/geolocation-plugin-release.aar or $OUT_DIR/android-artifacts/... as a fallback).
  4. Emits cargo:rustc-env=DIOXUS_ANDROID_ARTIFACT=<absolute path> so the Rust macro can reference the built artifact.

Metadata embedded in the Rust binary

In packages/geolocation/src/lib.rs we declare the Android artifact like this:

#[cfg(all(feature = "metadata", target_os = "android"))]
dioxus_platform_bridge::android_plugin!(
    plugin = "geolocation",
    aar = { env = "DIOXUS_ANDROID_ARTIFACT" },
    deps = ["implementation(\"com.google.android.gms:play-services-location:21.3.0\")"]
);

The aar block only needs a stable way to refer to the file that build.rs produced. Emitting a cargo:rustc-env=... from the build script is the idiomatic Rust approach for passing data from build.rs to the crate being compiled, which is why each plugin publishes its own environment variable. Nothing prevents you from using unique names—DIOXUS_GEO_ANDROID_ARTIFACT, DIOXUS_CAMERA_ANDROID_ARTIFACT, etc.—as long as your macro invocation references the same key that your build script sets. Automatically inferring the path from the plugin name is brittle because the artifact location lives in target/ (or another $OUT_DIR) that only build.rs knows about after running Gradle, so the build script remains the single source of truth.

The macro serializes:

  • plugin name
  • Absolute aar path (resolved via the env var emitted by build.rs)
  • Any Gradle dependency lines listed in deps

The metadata is wrapped in SymbolData::AndroidArtifact and stored under the shared __ASSETS__* symbol prefix (the same one used for assets and permissions). Nothing runs at runtime— the symbols are just scanned later by the CLI.

What the CLI does (dx serve --android)

  1. After building the Rust binary, dx walks every __ASSETS__* symbol once. When it encounters a SymbolData::AndroidArtifact, it adds it to the manifest alongside assets and permissions.
  2. install_android_artifacts() (in packages/cli/src/build/request.rs) copies each .aar into target/dx/.../app/libs/ in the generated Gradle project.
  3. For every artifact it ensures the Gradle dependency file contains both:
    • implementation(files("libs/<artifact>.aar"))
    • Any additional strings from deps (e.g. the Play Services location runtime)
  4. When Gradle runs for the host app, those libraries are already on disk and referenced in app/build.gradle.kts, so they get packaged automatically.

Note: because the .aar is built inside the Rust crate, plugin authors do not touch the app’s Gradle project. Everything happens by copying artifacts + editing app/build.gradle.kts inside the generated dx workspace.


iOS / macOS (Swift Package)

Source layout

  • Swift sources + Package.swift live under packages/geolocation/ios/
  • The Swift Package exports a single product: GeolocationPlugin

build.rs responsibilities

For Apple targets, build.rs:

  1. Detects the desired triple (aarch64-apple-ios, aarch64-apple-ios-sim, x86_64-apple-ios) and finds the matching SDK via xcrun --show-sdk-path.
  2. Runs xcrun swift build --package-path ios --configuration <debug|release> --triple <...> --sdk <...> --product GeolocationPlugin.
  3. Locates the resulting static library (libGeolocationPlugin.a) and emits:
    • cargo:rustc-link-search for both the Swift build output and the Xcode toolchain Swift libs
    • cargo:rustc-link-lib=static=GeolocationPlugin
    • cargo:rustc-link-lib=swiftCompatibility* + -force_load flags so the ObjC runtime sees the plugin class
    • cargo:rustc-link-lib=framework=CoreLocation / Foundation

At this point the Swift code is already linked into the Mach-O produced by Rust; no additional build steps are required later.

Metadata embedded in the Rust binary

In packages/geolocation/src/lib.rs we declare the Swift package via:

#[cfg(all(feature = "metadata", any(target_os = "ios", target_os = "macos")))]
dioxus_platform_bridge::ios_plugin!(
    plugin = "geolocation",
    spm = { path = "ios", product = "GeolocationPlugin" }
);

This writes an entry into the shared __ASSETS__* stream as SymbolData::SwiftPackage. Again, nothing is executed at runtime; it is just discoverable metadata alongside assets and permissions.

What the CLI does (dx serve --ios / --macos)

  1. The CLI sees the SymbolData::SwiftPackage entries while it’s already processing the asset stream.
  2. When at least one Swift package is present, embed_swift_stdlibs() runs (see packages/cli/src/build/request.rs). This invokes xcrun swift-stdlib-tool --scan-executable on the final app binary and copies the required Swift runtime libraries into the bundle’s Frameworks/ folder (iOS) or Contents/Frameworks/ (macOS).

Because the Swift package was linked during the Rust build, the CLI’s only job is to make sure the Swift runtime ships alongside the executable.


Summary table

Platform Native sources live Compiled by Metadata stored CLI responsibilities
Android packages/geolocation/android (Gradle lib) build.rs running gradlew assembleRelease SymbolData::AndroidArtifact (plugin, path, deps) Copy .aar into app/libs, append Gradle dependencies
iOS/macOS packages/geolocation/ios (SwiftPM) build.rs running xcrun swift build SymbolData::SwiftPackage (plugin, package path, product) Embed Swift stdlibs when Swift metadata is present

This setup keeps each plugin self-contained:

  • All native code + build tooling stays inside the crate.
  • build.rs produces platform artifacts and emits env vars for linker metadata.
  • The Dioxus CLI only needs the linker symbols to know what to copy/embed when bundling user apps.

New crates: (Please suggest the names as i am bad at naming stuff)

  • permissions: Public API that re-exports permissions-core and permissions-macro for easy integration.
  • permissions-core: Types for permission declaration and platform identifiers (Android, IOS, MacOS) (We can extend the identifiers as needed or requested)
  • permissions-macro: Procedural Macros (static_permission!) for declaring permissions via linker symbol (So cli can extract the symbols and update the manifest files automatically)
  • dx-macro-helpers: Common helpers for linker based stuff, shared by manganis and permission
  • platform-bridge: Bareminimal Cross-platform FFI utilities and plugin metadata for Android (JNI) and Darwin (objc2)
  • platform-bridge-macro: android_plugin! macro for embedding Java source file paths in binaries via linker symbols

wheregmis avatar Oct 26 '25 01:10 wheregmis

Yey, Android Permission Dialog is working. Screenshot_1761495440

wheregmis avatar Oct 26 '25 16:10 wheregmis

Web: image

wheregmis avatar Oct 27 '25 04:10 wheregmis

MacOs: image

wheregmis avatar Oct 27 '25 04:10 wheregmis

@ealmloff Thank you for the feedback. Other than these nits, What do you think about the overall process? Do you think this is something we should proceed with to upstream it? Or maybe try and figure some other build processes, toolings? I want to discuss some top level ideas on the whole process and how should we proceed.

wheregmis avatar Nov 05 '25 01:11 wheregmis

@ealmloff Thank you for the feedback. Other than these nits, What do you think about the overall process? Do you think this is something we should proceed with to upstream it? Or maybe try and figure some other build processes, toolings? I want to discuss some top level ideas on the whole process and how should we proceed.

@jkelleyrtp may have more thoughts here as he set up most of the android/ios build support in the CLI

From my perspective, I think this is close to the right approach, but I'm not sure this is the right interface yet. The two approaches for collecting bindings seem to be either build scripts (like robius and tauri) or linker-based collection like this and wasm-bindgen. Between the two linker-based collection is a lot easier to consume which will be important as the number of native bindings grows.

Both tauri or robius require you to manually register permissions which adds extra setup for any system libraries. That approach makes permissions more visible, but automatically registering permissions is much easier to use. We may want to think about adding a prompt to accept or reject the new permissions collected by the linker, but overall I think the linker permissions approach is much nicer.

One concern with the linker approach for bindings is symbols that are optimized away. Looking at the example linked in your description, it looks like you/AI had some issues with symbols being dropped that required adding __ensure_permissions_linked. We will need to either make sure the symbol isn't dropped within the macro by adding #[used] or tie the symbol to the usage of the script that is linked so it is only dropped if the bindings are not used

ealmloff avatar Nov 05 '25 14:11 ealmloff

@ealmloff Thank you for the feedback. Other than these nits, What do you think about the overall process? Do you think this is something we should proceed with to upstream it? Or maybe try and figure some other build processes, toolings? I want to discuss some top level ideas on the whole process and how should we proceed.

@jkelleyrtp may have more thoughts here as he set up most of the android/ios build support in the CLI

From my perspective, I think this is close to the right approach, but I'm not sure this is the right interface yet. The two approaches for collecting bindings seem to be either build scripts (like robius and tauri) or linker-based collection like this and wasm-bindgen. Between the two linker-based collection is a lot easier to consume which will be important as the number of native bindings grows.

Both tauri or robius require you to manually register permissions which adds extra setup for any system libraries. That approach makes permissions more visible, but automatically registering permissions is much easier to use. We may want to think about adding a prompt to accept or reject the new permissions collected by the linker, but overall I think the linker permissions approach is much nicer.

One concern with the linker approach for bindings is symbols that are optimized away. Looking at the example linked in your description, it looks like you/AI had some issues with symbols being dropped that required adding __ensure_permissions_linked. We will need to either make sure the symbol isn't dropped within the macro by adding #[used] or tie the symbol to the usage of the script that is linked so it is only dropped if the bindings are not used

Yes, __ensure_permissions_linked does need some nits, i agree on that part. I started with build script like how robius was working but its a hastle to build dex files copy it and alot of issues when the native plugins will grow. Supporting tauri native plugins would be awesome but i tried and failed as it needs some extra setups that we need to deal with to provide seamless experience. With the growing number of contributors and community i think we can definately provide better abstraction and maybe just sharing android/kotlin files with those plugins should be good. Also, another thing is instead of .java file only, i want to support ,kt file for registering kotlin files and placing it on copying it on correct path for kotlin so we dont have to use java only. (Some plugins are freely available for kotlin and java side of things). Another discussion is on the IOS side of things, building and supporting each native swift package is tricky and challenging like how we are providing seamless for android but i am also not sure if we can do almost everything with objc crates. But the way its growing i think we should be able to do almost majority of things with it so not handling each ios plugin as a native swift package + swift files seems more appealing to me atm.

wheregmis avatar Nov 05 '25 15:11 wheregmis

Going through the linker seems to be the right approach for permissions. Both build scripts and linker will require an end tool to collect the permissions, and I much prefer infrastructure-as-code approaches than the implicit build script. It's nice that not using a permission means not inserting it the Info.plist. Instead of #[used] attributes I would prefer that we make the permission a required argument to the function that requests the permission, and then we perform a volatile read, forcing the symbol to remain. This is similar to how Asset remains because its Display impl forces a volatile read.

We might need to think about Koitlin/Swift source files a bit more since cargo's cache busting system will be more reliable than the linker-based system, at least early on.

However, I'd rather not create a new symbol type for collecting permissions and instead try to reuse the one we use for assets. Though, the asset symbol format is not very flexible (see https://github.com/DioxusLabs/dioxus/issues/4863), so it might require some work on our end to allow different variants as long as they're the same size. Adding a new symbol seems to require duplicating logic in the CLI which we can avoid if we stick with one symbol with different variants.


Also, FWIW, this would be at the top of the list for 0.8 features. If we could get it in 0.7, then it would be at the top of priority behind fixing bugs, that way we can ship native apis while still on 0.7

jkelleyrtp avatar Nov 06 '25 19:11 jkelleyrtp

@jkelleyrtp Thank you for the inputs. All the recommendation and inputs seems super valid and reasonable. After looking into #4863 I do think if we can somehow provide a appropriate way of creating different variants, and we dont need to create multiple symbols, alot of code on the CLI for extracting symbols can be alot more refined and cleaned. For kotlin/swift sources files, I also dont have super huge usecase to get into alot more depth to learn and define the fundamentals so i would definately leave these big chunks to dioxus core members. For the time being i will try to update my codes to more like an enum of LinkerSymbol which can hold either asset or permission and try to cleanup the CLI code abit so we dont go with multiple symbol type and can slip the permissions under MANGANIS symbol. And when we have a concrete plan for #4863 I can update my code for it. Also maybe we can try to slip in the permissions related stuff if possible into the 0.7 that way in with permissions, followed by build system for kotlin/swift and native apis can be added and shipped while still on 0.7.

wheregmis avatar Nov 06 '25 20:11 wheregmis

I think i am able to package swift files as swift package and package it and run. (Like how tauri does for swift files)

wheregmis avatar Nov 13 '25 19:11 wheregmis

I am also able to build each android plugin as a gradle submodule and can be tested like this inside each plugin

./gradlew assembleRelease --no-daemon --warning-mode all

-Oops i broke it lol- my bad it worked

wheregmis avatar Nov 13 '25 21:11 wheregmis

So the current plugin structure is something like this. image

wheregmis avatar Nov 13 '25 21:11 wheregmis

Running gradle assembly takes time now, And i expect with the number of apis you add, it will grow exponentially

wheregmis avatar Nov 13 '25 21:11 wheregmis

Here is a breakdown of how things are working for now.

Android (Kotlin / Gradle)

Source layout

  • Kotlin sources + Gradle wrapper live under packages/geolocation/android/
  • The module builds a release .aar: android/build/outputs/aar/geolocation-plugin-release.aar

build.rs responsibilities

packages/geolocation/build.rs builds the Gradle module whenever the target triple contains android:

  1. Respects the toolchain env exported by the CLI (DX_ANDROID_JAVA_HOME, DX_ANDROID_SDK_ROOT, DX_ANDROID_NDK_HOME, DX_ANDROID_ARTIFACT_DIR). If those are absent (non-dx builds) it falls back to ANDROID_* or JAVA_HOME.
  2. Runs ./gradlew assembleRelease inside packages/geolocation/android.
  3. Copies the resulting .aar into the CLI-provided artifact staging dir ($DX_ANDROID_ARTIFACT_DIR/geolocation-plugin-release.aar or $OUT_DIR/android-artifacts/... as a fallback).
  4. Emits cargo:rustc-env=DIOXUS_ANDROID_ARTIFACT=<absolute path> so the Rust macro can reference the built artifact.

Metadata embedded in the Rust binary

In packages/geolocation/src/lib.rs we declare the Android artifact like this:

#[cfg(all(feature = "metadata", target_os = "android"))]
dioxus_platform_bridge::android_plugin!(
    plugin = "geolocation",
    aar = { env = "DIOXUS_ANDROID_ARTIFACT" },
    deps = ["implementation(\"com.google.android.gms:play-services-location:21.3.0\")"]
);

The aar block only needs a stable way to refer to the file that build.rs produced. Emitting a cargo:rustc-env=... from the build script is the idiomatic Rust approach for passing data from build.rs to the crate being compiled, which is why each plugin publishes its own environment variable. Nothing prevents you from using unique names—DIOXUS_GEO_ANDROID_ARTIFACT, DIOXUS_CAMERA_ANDROID_ARTIFACT, etc.—as long as your macro invocation references the same key that your build script sets. Automatically inferring the path from the plugin name is brittle because the artifact location lives in target/ (or another $OUT_DIR) that only build.rs knows about after running Gradle, so the build script remains the single source of truth.

The macro serializes:

  • plugin name
  • Absolute aar path (resolved via the env var emitted by build.rs)
  • Any Gradle dependency lines listed in deps

The metadata is wrapped in SymbolData::AndroidArtifact and stored under the shared __ASSETS__* symbol prefix (the same one used for assets and permissions). Nothing runs at runtime— the symbols are just scanned later by the CLI.

What the CLI does (dx serve --android)

  1. After building the Rust binary, dx walks every __ASSETS__* symbol once. When it encounters a SymbolData::AndroidArtifact, it adds it to the manifest alongside assets and permissions.
  2. install_android_artifacts() (in packages/cli/src/build/request.rs) copies each .aar into target/dx/.../app/libs/ in the generated Gradle project.
  3. For every artifact it ensures the Gradle dependency file contains both:
    • implementation(files("libs/<artifact>.aar"))
    • Any additional strings from deps (e.g. the Play Services location runtime)
  4. When Gradle runs for the host app, those libraries are already on disk and referenced in app/build.gradle.kts, so they get packaged automatically.

Note: because the .aar is built inside the Rust crate, plugin authors do not touch the app’s Gradle project. Everything happens by copying artifacts + editing app/build.gradle.kts inside the generated dx workspace.


iOS / macOS (Swift Package)

Source layout

  • Swift sources + Package.swift live under packages/geolocation/ios/
  • The Swift Package exports a single product: GeolocationPlugin

build.rs responsibilities

For Apple targets, build.rs:

  1. Detects the desired triple (aarch64-apple-ios, aarch64-apple-ios-sim, x86_64-apple-ios) and finds the matching SDK via xcrun --show-sdk-path.
  2. Runs xcrun swift build --package-path ios --configuration <debug|release> --triple <...> --sdk <...> --product GeolocationPlugin.
  3. Locates the resulting static library (libGeolocationPlugin.a) and emits:
    • cargo:rustc-link-search for both the Swift build output and the Xcode toolchain Swift libs
    • cargo:rustc-link-lib=static=GeolocationPlugin
    • cargo:rustc-link-lib=swiftCompatibility* + -force_load flags so the ObjC runtime sees the plugin class
    • cargo:rustc-link-lib=framework=CoreLocation / Foundation

At this point the Swift code is already linked into the Mach-O produced by Rust; no additional build steps are required later.

Metadata embedded in the Rust binary

In packages/geolocation/src/lib.rs we declare the Swift package via:

#[cfg(all(feature = "metadata", any(target_os = "ios", target_os = "macos")))]
dioxus_platform_bridge::ios_plugin!(
    plugin = "geolocation",
    spm = { path = "ios", product = "GeolocationPlugin" }
);

This writes an entry into the shared __ASSETS__* stream as SymbolData::SwiftPackage. Again, nothing is executed at runtime; it is just discoverable metadata alongside assets and permissions.

What the CLI does (dx serve --ios / --macos)

  1. The CLI sees the SymbolData::SwiftPackage entries while it’s already processing the asset stream.
  2. When at least one Swift package is present, embed_swift_stdlibs() runs (see packages/cli/src/build/request.rs). This invokes xcrun swift-stdlib-tool --scan-executable on the final app binary and copies the required Swift runtime libraries into the bundle’s Frameworks/ folder (iOS) or Contents/Frameworks/ (macOS).

Because the Swift package was linked during the Rust build, the CLI’s only job is to make sure the Swift runtime ships alongside the executable.


Summary table

Platform Native sources live Compiled by Metadata stored CLI responsibilities
Android packages/geolocation/android (Gradle lib) build.rs running gradlew assembleRelease SymbolData::AndroidArtifact (plugin, path, deps) Copy .aar into app/libs, append Gradle dependencies
iOS/macOS packages/geolocation/ios (SwiftPM) build.rs running xcrun swift build SymbolData::SwiftPackage (plugin, package path, product) Embed Swift stdlibs when Swift metadata is present

This setup keeps each plugin self-contained:

  • All native code + build tooling stays inside the crate.
  • build.rs produces platform artifacts and emits env vars for linker metadata.
  • The Dioxus CLI only needs the linker symbols to know what to copy/embed when bundling user apps.

wheregmis avatar Nov 13 '25 22:11 wheregmis

I have added the geolocation package + example for geolocation in for easy testing so the diff is huge as of now. I need to delete them before we upstream it.

wheregmis avatar Nov 13 '25 22:11 wheregmis

image image image

wheregmis avatar Nov 13 '25 23:11 wheregmis

@jkelleyrtp I am sorry for a large diff atm, it has changes from #4932 as well as package/geolocation and example inside. So its easier for you to check. I am going out for few weeks after 2-3 days so i wanted to make sure this PR is in good shape before that. A small review on the build process would be really appreciated. I think the way it turned out is really interesting + approach is something that can be shared across other rust UI projects (I wonder someone can fork tauri plugins and make it compatiable with dioxus as this opens the door for that as well)

wheregmis avatar Nov 13 '25 23:11 wheregmis

@jkelleyrtp I am sorry for a large diff atm, it has changes from #4932 as well as package/geolocation and example inside. So its easier for you to check. I am going out for few weeks after 2-3 days so i wanted to make sure this PR is in good shape before that. A small review on the build process would be really appreciated. I think the way it turned out is really interesting + approach is something that can be shared across other rust UI projects (I wonder someone can fork tauri plugins and make it compatiable with dioxus as this opens the door for that as well)

Awesome! Will try to get it in next week and then hopefully in a patch release soon after that

jkelleyrtp avatar Nov 14 '25 14:11 jkelleyrtp

as someone who will need native permissions for usbc and bluetooth access on android and iOs i love seeing this being built. thank you team and everyone involved

hogyzen12 avatar Nov 20 '25 02:11 hogyzen12