permissions, manganis::ffi, full Info.plist/AndroidManifest.xml customization (#4842)

* initial version

* make custom permission work without const-vec change

* wiring up with cli

* yey symbol found

* wip

* holy moly

* proper ui

* cleanup

* okay

* wip cleanup

* reduce boilerplate code into mobile-core

* Delete copilot-instructions.md

* revert hello-world example for low diff

* fix android build in mobile-core

* rename permission to static_permission

* small success with popup location on android

* android popup working

* Refactor permission checks for readability

Improves code formatting in has_location_permission and check_permission functions by splitting long lines and updating function signatures for better readability. No functional changes were made.

* wip consolidate all java files and build from cli instead of build.rs

* few cli cleanups

* wip java_plugin macro

* cleanup cleanup cleanup

* Update java_plugin macro to use full relative file paths

The java_plugin macro now accepts full relative paths for Java files instead of assuming a fixed directory structure. Documentation and usage examples have been updated to reflect this change, improving flexibility and clarity for specifying file locations.

* Improve iOS geolocation permission and retrieval logic

Added checks for authorization status before requesting location permissions and retrieving location data. Now only requests permission if not determined, and attempts to retrieve cached location before starting location updates, improving efficiency and user experience.

* Add ios_plugin macro for iOS framework metadata

Introduces the ios_plugin! macro in mobile-core-macro for declarative iOS framework metadata embedding. Updates mobile-core to re-export the macro and refactors mobile-geolocation to use ios_plugin! instead of manual linker attributes.

* not needed

* simplify the plugin

* rename mobile-core to platform-bridge and mobile-core-macro to platform-bridge-macro

* Refactor metadata serialization buffer initialization

Replaces inline creation of ConstVec with a named EMPTY constant for buffer initialization in metadata serialization. This improves code clarity and maintainability.

* mobile-geolocation out of dioxus

* take the example also out of dioxus

* remove example from cargo toml

* remove demo css

* revert hello-world example

* revert mac.plist

* revert gitignore

* Remove basic usage example for permissions crate

Deleted the basic_usage.rs example file from the permissions crate. This change may be part of a cleanup or refactoring to remove outdated or unnecessary example code.

* Delete plan.md

* remove dups

* cleanup java build

* cleanup permission symbol extraction

* update todo

* small slop of tested permissions in enums for minimum diff

* Refactor symbol extraction into linker_symbols module

Moved generic symbol extraction logic from android_java.rs and permissions.rs into a new linker_symbols.rs module. Updated both files to use the shared utilities for finding symbol offsets, reducing code duplication and improving maintainability.

* wip consolidate ios and macos as darwin

* Add docs and re-export for MainThreadCell and MainThreadMarker

Expanded documentation for MainThreadCell, clarifying usage and safety. Changed visibility of MainThreadCell to public, improved method docs, and re-exported MainThreadMarker in darwin/mod.rs for easier access.

* Update Darwin framework symbol and section names

Changed the framework symbol prefix from '__DARWIN_FRAMEWORK__' to '__DARWIN_FW__' and updated the Mach-O section name from '__darwin_framework' to '__darwin_fw' for compliance with section name length limits and consistency.

* Fix Info.plist path handling for iOS and macOS

Adds comments and corrects the path to Info.plist for iOS and macOS app bundles. For iOS, Info.plist is at the root of the .app bundle, while for macOS it is inside Contents/. Also improves warning message to include the missing path.

* Handle missing permission symbols and web bundle case

Improves permission extraction by returning an empty manifest when no permission symbols are found or when building a web bundle, as permissions are runtime-only in that case. Adds debug logging for missing symbols and ensures robust handling of these scenarios.

* objc2 over framework

* windows, linux, web all permissions are runtime so cleanup

* only include permissions for build time

* cleanup unnecessary test

* more cleanup

* extract shared into dx-macro-helpers

* rc.3 to 0.7.0 for const-serialize

* Add CustomPermissionBuilder and enhance PermissionBuilder for better permission management

- Introduced `CustomPermissionBuilder` for creating custom permissions with platform-specific identifiers.
- Enhanced `PermissionBuilder` to support custom permissions and location permissions with a clear builder pattern.
- Updated documentation and examples in the macro README to reflect new usage patterns for custom and location permissions.
- Improved parsing logic in the macro to handle both builder and direct construction patterns for permissions.

* clippy fixes

* implement const cbor encoding + decoding for basic types

* Add #[used] attribute to linker sections for optimization protection

- Added #[used] attribute to linker sections in both permission and Android plugin macros to prevent the linker from optimizing away symbols, ensuring they are preserved even if unused.
- Created static references to linker sections to enhance defense-in-depth against optimization issues.

* implement map encoding and decoding

* fix map test

* wip unified linker symbol

* cleanup permissions

* pipe java sources with manganis as well

* all tests passing

* dynamically sized arrays

* smaller str encoding

* swap array and list and add more documentation

* move serialization and deserialization for each struct together

* support both legacy and new assets

* bump const serialize version

* fix asset conversion code

* pull out manganis symbol struct from tuble

* test reading old asset versions

* more list/array cleanup and bump rust version

* fix clippy

* Revert "pipe java sources with manganis as well"

This reverts commit ddea9a459d9ac26f3f037a74d9ae0de3edfced67.

* Revert "cleanup permissions"

This reverts commit 7b18500155237403e179f0783cb8903a4f158b80.

* Revert "wip unified linker symbol"

This reverts commit 813e33feddb30d26868f6ac6e6da318402ac138f.

* Revert "Add #[used] attribute to linker sections for optimization protection"

This reverts commit a94a7a3a5e8eee80060de9d66a53aabfcf554f96.

* update cargo lock

* wip permission through assets

* abit cleanup

* fix reexports

* fix assets

* Fix the permissions piping

* some small cleanups

* checkout const-serialize as per #4932 and fix build

* invoking swift as a package

* geolocation package & example dumping for easy testing

* keep geolocation simple for testing

* cleanups

* pipe everything through __ASSETS__ and cleanup

* remove linker_symbols helper file

* example

* re example

* copy build methos into mobile-plugin-build crate

* Add conditional import for dioxus_07 feature

Introduces a conditional import to use the dioxus_07 crate when the corresponding feature is enabled, improving compatibility with different dioxus versions.

* Improve error handling and JNI reference usage

Adds a null check for Objective-C instance initialization in the iOS geolocation plugin to prevent errors on failed allocation. Refactors Android callback to use local JNI references instead of creating unnecessary global references, reducing potential memory leaks.

* revert changes on playwright

* Update dioxus dependency to 0.7.2 in Cargo.lock

Removed dioxus 0.7.1 from dependencies, retaining only dioxus 0.7.2 for the dioxus-cli-optimization-test package.

* small cleanup

* Add legacy const-serialize 0.7 support to dx-macro-helpers

Introduces support for the const-serialize 0.7 format in dx-macro-helpers by adding a new dependency and export, as well as a helper function for serialization with layout padding. Updates manganis to use the new helper for serializing assets in the legacy format, simplifying code and improving maintainability.

* Add helper for legacy and current linker sections

Introduces generate_link_sections_with_legacy in dx-macro-helpers to emit both legacy and current linker sections for compatibility. Refactors manganis-macro to use this new helper, reducing duplication and improving maintainability.

* Fix lints

* Fix clippy and doctest

* Refactor asset bundling and allow large enum variant

Refactored Asset::bundled to use Vec<u8> instead of ConstVec for reading volatile slices, simplifying the code and improving compatibility. Added #[allow(clippy::large_enum_variant)] to SymbolData enum to suppress Clippy warnings about large enum variants.

* Update license URLs in README to use angle brackets

Changed license URLs in the README from parentheses to angle brackets for improved formatting and clarity.

* Fix asset deserialization with correct buffer type

Replaces the use of `bytes.read()` with `ConstReadBuffer::new(bytes.as_ref())` for asset deserialization, ensuring compatibility with the expected deserialization API.

* Refactor geolocation and asset handling for clarity

Removed unnecessary clones in geolocation example event handlers to simplify state management. Updated SymbolDataOrAsset to store SymbolData in a Box for improved memory handling and adjusted related logic. Fixed documentation example in geolocation lib to include PermissionState in the import.

* Update geolocation example and permissions usage docs

Removes unused variables in the geolocation example's App function. Updates the permissions crate documentation to reflect new API usage with PermissionBuilder and LocationPrecision.

* Update permissions-macro to use permissions crate

Switched imports and dev-dependency from permissions_core to permissions in permissions-macro. Updated documentation and examples to reflect the new crate usage.

* rollback const-serialize changes

* move items around (doesn't compile)

* convert gelocation plugin to example

* remove dx-macro-helpers

* we're just gonna full-send linker everything

* rollback desktop changes

* remove gradle

* fix most compile?

* fix dioxus vers

* small cleanups

* add claude design docs

* add ffi macro

* remove

* loads but not linking yet

* fix output leak

* a little bit closer but not quite

* improve android code gen

* sheeeeee works!!!

* small tweaks to demo

* replace ios/android plugin with ffi macro entirely

* simplify ffi module

* simplify again

* condense / rip out some stuff from manganis

* remove types form public api

* wip, permissions

* oops I broke it

* I unbroke it

* move arch doc

* remove extra manifest types

* drop bindgen file

* remove permission

* small cleanup

* remove unsafecall mtm thing

* yoink perms

* wire up manifest

* Add deeplinks

* add schema generation

* update schema

* wip....

* fix recurse limit schema generation

* wip, muck around with schema

* add inline dioxus.toml support

* merge bundle config schemas

* claude my man

* Remove cdecl, annotate ffi

* rename design docs

* wip: live activities

* wip: apple plugin bundling

* wip

* get live activities linking properly

* small cleanups

* remove sidecars

* remove widget

* yoink legacy

* cleanup

* more fixups

* wire up more things to dioxus.toml

* fix workspace join

* final cleanups

* fmt

* final bit of clippy

* ci tweak

* update schema

* fmt, doc

* update schema docs

---------

Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-02-13 15:39:38 -08:00
1 parent 64cc42e16e
commit eeb60f7173
73 files changed
+13767 -336

No files matched your search

+7 -5
View File
@@ -22,8 +22,10 @@ use std::{fmt::Debug, hash::Hash, path::PathBuf};
pub struct BundledAsset {
/// The absolute path of the asset
absolute_source_path: ConstStr,
/// The bundled path of the asset
bundled_path: ConstStr,
/// The options for the asset
options: AssetOptions,
}
@@ -147,32 +149,32 @@ impl Asset {
pub fn bundled(&self) -> BundledAsset {
// Read the slice using volatile reads to prevent the compiler from optimizing
// away the read at compile time
fn read_slice_volatile(bundled: &'static [u8]) -> const_serialize_07::ConstVec<u8> {
fn read_slice_volatile(bundled: &'static [u8]) -> Vec<u8> {
let ptr = bundled as *const [u8] as *const u8;
let len = bundled.len();
if ptr.is_null() {
panic!("Tried to use an asset that was not bundled. Make sure you are compiling dx as the linker");
}
let mut bytes = const_serialize_07::ConstVec::new();
let mut bytes = Vec::with_capacity(len);
for byte in 0..len {
// SAFETY: We checked that the pointer was not null above. The pointer is valid for reads and
// since we are reading a u8 there are no alignment requirements
let byte = unsafe { std::ptr::read_volatile(ptr.add(byte)) };
bytes = bytes.push(byte);
bytes.push(byte);
}
bytes
}
let bundled = (self.bundled)();
let bytes = read_slice_volatile(bundled);
let read = bytes.as_ref();
let read = bytes.as_slice();
let asset = deserialize_const!(BundledAsset, read).expect("Failed to deserialize asset. Make sure you built with the matching version of the Dioxus CLI").1;
// If the asset wasn't bundled with the newer format, try the legacy format
if asset.bundled_path() == BundledAsset::PLACEHOLDER_HASH {
let bundled = (self.legacy)();
let bytes = read_slice_volatile(bundled);
let read = bytes.read();
let read = const_serialize_07::ConstReadBuffer::new(bytes.as_ref());
let asset = const_serialize_07::deserialize_const!(BundledAsset, read).expect("Failed to deserialize asset. Make sure you built with the matching version of the Dioxus CLI").1;
asset
} else {
+123
View File
@@ -0,0 +1,123 @@
use crate::BundledAsset;
use const_serialize::{ConstStr, SerializeConst};
use const_serialize_08 as const_serialize;
use std::hash::Hash;
/// Unified symbol data that can represent both assets and permissions
///
/// This enum is used to serialize different types of metadata into the binary
/// using the same `__ASSETS__` symbol prefix. The CBOR format allows for
/// self-describing data, making it easy to add new variants in the future.
///
/// Variant order does NOT matter for CBOR enum serialization - variants are
/// matched by name (string), not by position or tag value.
#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeConst)]
#[repr(C, u8)]
#[allow(clippy::large_enum_variant)]
#[non_exhaustive]
pub enum SymbolData {
/// An asset that should be bundled with the application
Asset(BundledAsset),
/// Android plugin metadata (prebuilt artifacts + Gradle deps)
AndroidArtifact(AndroidArtifactMetadata),
/// Swift package metadata (SPM location + product)
SwiftPackage(SwiftPackageMetadata),
}
/// Platform categories for permission mapping
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SerializeConst)]
pub enum Platform {
Android,
Ios,
Macos,
}
/// Bit flags for supported platforms
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SerializeConst)]
pub struct PlatformFlags(u8);
impl PlatformFlags {
pub const fn new() -> Self {
Self(0)
}
}
impl Default for PlatformFlags {
fn default() -> Self {
Self::new()
}
}
impl PlatformFlags {
pub const fn with_platform(mut self, platform: Platform) -> Self {
self.0 |= 1 << platform as u8;
self
}
pub const fn supports(&self, platform: Platform) -> bool {
(self.0 & (1 << platform as u8)) != 0
}
pub const fn all() -> Self {
Self(0b000111) // Android + iOS + macOS
}
pub const fn mobile() -> Self {
Self(0b000011) // Android + iOS
}
}
/// Platform-specific permission identifiers
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PlatformIdentifiers {
pub android: Option<ConstStr>,
pub ios: Option<ConstStr>,
pub macos: Option<ConstStr>,
}
/// Metadata describing an Android plugin artifact (.aar) that must be copied into the host Gradle project.
#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeConst)]
pub struct AndroidArtifactMetadata {
pub plugin_name: ConstStr,
pub artifact_path: ConstStr,
pub gradle_dependencies: ConstStr,
}
impl AndroidArtifactMetadata {
pub const fn new(
plugin_name: &'static str,
artifact_path: &'static str,
gradle_dependencies: &'static str,
) -> Self {
Self {
plugin_name: ConstStr::new(plugin_name),
artifact_path: ConstStr::new(artifact_path),
gradle_dependencies: ConstStr::new(gradle_dependencies),
}
}
}
/// Metadata for a Swift package that needs to be linked into the app (iOS/macOS).
#[derive(Debug, Clone, Copy, PartialEq, Eq, SerializeConst)]
pub struct SwiftPackageMetadata {
pub plugin_name: ConstStr,
pub package_path: ConstStr,
pub product: ConstStr,
}
impl SwiftPackageMetadata {
pub const fn new(
plugin_name: &'static str,
package_path: &'static str,
product: &'static str,
) -> Self {
Self {
plugin_name: ConstStr::new(plugin_name),
package_path: ConstStr::new(package_path),
product: ConstStr::new(product),
}
}
}
@@ -21,3 +21,6 @@ pub use css_module::*;
mod css_module_parser;
pub use css_module_parser::*;
mod ffi;
pub use ffi::*;
@@ -1,4 +1,4 @@
use crate::{resolve_path, AssetParseError};
use crate::{linker::generate_link_section, resolve_path, AssetParseError};
use macro_string::MacroString;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
@@ -88,7 +88,7 @@ impl AssetParser {
// Generate the link section for the asset. The link section includes the source path and the
// output path of the asset. We force the asset to be included in the binary even if it is unused
// if the asset is unhashed
let link_section = crate::generate_link_section(quote!(__ASSET), &asset_hash);
let link_section = generate_link_section(quote!(__ASSET), &asset_hash);
// generate the asset::new method to deprecate the `./assets/blah.css` syntax
let constructor = if asset.is_relative() {
File diff suppressed because it is too large. Load diff
+61 -3
View File
@@ -13,10 +13,9 @@ use syn::{
pub(crate) mod asset;
pub(crate) mod css_module;
pub(crate) mod ffi;
pub(crate) mod linker;
use linker::generate_link_section;
use crate::css_module::{expand_css_module_struct, CssModuleAttribute};
/// The asset macro collects assets that will be included in the final binary
@@ -208,7 +207,7 @@ pub fn option_asset(input: TokenStream) -> TokenStream {
/// fn app() -> Element {
/// #[css_module("/assets/styles.css")]
/// struct Styles;
///
///
/// rsx! {
/// div { class: Styles::container,
/// button { class: Styles::button, "Click me" }
@@ -226,6 +225,65 @@ pub fn css_module(input: TokenStream, item: TokenStream) -> TokenStream {
tokens.into()
}
/// Generate FFI bindings between Rust and native platforms (Swift/Kotlin)
///
/// This attribute macro parses an `extern "Swift"` or `extern "Kotlin"` block and generates:
/// 1. Opaque type wrappers for foreign types
/// 2. Function implementations with direct JNI/ObjC bindings
/// 3. Linker metadata for the CLI to compile the native source
///
/// # Syntax
///
/// ```rust,ignore
/// #[manganis::ffi("/src/ios")]
/// extern "Swift" {
/// pub type GeolocationPlugin;
/// pub fn get_position(this: &GeolocationPlugin, high_accuracy: bool) -> Option<String>;
/// }
///
/// #[manganis::ffi("/src/android")]
/// extern "Kotlin" {
/// pub type GeolocationPlugin;
/// pub fn get_position(this: &GeolocationPlugin, high_accuracy: bool) -> Option<String>;
/// }
/// ```
///
/// # Path Parameter
///
/// The path in the attribute specifies the native source folder relative to `CARGO_MANIFEST_DIR`:
/// - For Swift: A SwiftPM package folder containing `Package.swift`
/// - For Kotlin: A Gradle project folder containing `build.gradle.kts`
///
/// # Type Declarations
///
/// Use `type Name;` to declare opaque foreign types. These become Rust structs wrapping
/// the native object handle (GlobalRef for JNI, raw pointer for ObjC).
///
/// # Function Declarations
///
/// Functions can be:
/// - **Instance methods**: First argument is `this: &TypeName`
/// - **Static methods**: No `this` argument
///
/// # Supported Types
///
/// - Primitives: `bool`, `i8`-`i64`, `u8`-`u64`, `f32`, `f64`
/// - Strings: `String`, `&str`
/// - Options: `Option<T>` where T is supported
/// - Opaque refs: `&TypeName` for foreign type references
#[proc_macro_attribute]
pub fn ffi(attr: TokenStream, item: TokenStream) -> TokenStream {
use ffi::{FfiAttribute, FfiBridgeParser};
let attr = parse_macro_input!(attr as FfiAttribute);
let item = parse_macro_input!(item as syn::ItemForeignMod);
match FfiBridgeParser::parse_with_attr(attr, item) {
Ok(parser) => parser.generate().into(),
Err(err) => err.to_compile_error().into(),
}
}
fn resolve_path(raw: &str, span: Span) -> Result<PathBuf, AssetParseError> {
// Get the location of the root of the crate which is where all assets are relative to
//
+70 -30
View File
@@ -1,43 +1,83 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use quote::ToTokens;
/// We store description of the assets an application uses in the executable.
/// We use the `link_section` attribute embed an extra section in the executable.
/// We force rust to store a serialized representation of the asset description
/// inside a particular region of the binary, with the label "manganis".
/// After linking, the "manganis" sections of the different object files will be merged.
pub fn generate_link_section(asset: impl ToTokens, asset_hash: &str) -> TokenStream2 {
/// Generate a linker section for embedding arbitrary data in the binary
///
/// This is a generic version that allows customizing the serialization function,
/// buffer type, and copy bytes function. Used by both asset and FFI metadata embedding.
pub fn generate_link_section_inner(
item: TokenStream2,
hash: &str,
prefix: &str,
serialize_fn: TokenStream2,
copy_bytes_fn: TokenStream2,
buffer_type: TokenStream2,
) -> TokenStream2 {
let position = proc_macro2::Span::call_site();
let export_name = syn::LitStr::new(&format!("__ASSETS__{}", asset_hash), position);
let legacy_export_name = syn::LitStr::new(&format!("__MANGANIS__{}", asset_hash), position);
quote::quote! {
// We bundle both the legacy and new link sections for compatibility with older CLIs
static __LEGACY_LINK_SECTION: &'static [u8] = {
// First serialize the asset into a constant sized buffer
const __BUFFER: manganis::macro_helpers::const_serialize_07::ConstVec<u8> = manganis::macro_helpers::serialize_asset_07(&#asset);
// Then pull out the byte slice
const __BYTES: &[u8] = __BUFFER.as_ref();
// And the length of the byte slice
const __LEN: usize = __BYTES.len();
// Now that we have the size of the asset, copy the bytes into a static array
#[unsafe(export_name = #legacy_export_name)]
static __LINK_SECTION: [u8; __LEN] = manganis::macro_helpers::copy_bytes(__BYTES);
&__LINK_SECTION
};
let export_name = syn::LitStr::new(&format!("{}{}", prefix, hash), position);
quote! {
#[used]
static __LINK_SECTION: &'static [u8] = {
// First serialize the asset into a constant sized buffer
const __BUFFER: manganis::macro_helpers::const_serialize::ConstVec<u8> = manganis::macro_helpers::serialize_asset(&#asset);
// Then pull out the byte slice
const __BUFFER: #buffer_type = #serialize_fn(&#item);
const __BYTES: &[u8] = __BUFFER.as_ref();
// And the length of the byte slice
const __LEN: usize = __BYTES.len();
// Now that we have the size of the asset, copy the bytes into a static array
#[unsafe(export_name = #export_name)]
static __LINK_SECTION: [u8; __LEN] = manganis::macro_helpers::copy_bytes(__BYTES);
#[used]
static __LINK_SECTION: [u8; __LEN] = #copy_bytes_fn(__BYTES);
&__LINK_SECTION
};
}
}
/// Generate a linker section for embedding asset data in the binary
///
/// This function creates a static array containing the serialized asset data
/// and exports it with the __ASSETS__ prefix for unified symbol collection.
/// Uses the generic linker helper from dx-macro-helpers for consistency.
pub fn generate_link_section(asset: impl ToTokens, asset_hash: &str) -> TokenStream2 {
let item = asset;
let hash: &str = asset_hash;
let prefix_current: &str = "__ASSETS__";
let prefix_legacy: &str = "__MANGANIS__";
let serialize_fn_current = quote! { manganis::macro_helpers::serialize_asset };
let serialize_fn_legacy = quote! { manganis::macro_helpers::serialize_asset_07 };
let copy_bytes_fn = quote! { manganis::macro_helpers::copy_bytes };
let buffer_type_current = quote! { manganis::macro_helpers::ConstVec<u8, 4096> };
let buffer_type_legacy = quote! { manganis::macro_helpers::const_serialize_07::ConstVec<u8> };
let position = proc_macro2::Span::call_site();
let export_name = syn::LitStr::new(&format!("{}{}", prefix_current, hash), position);
let legacy_export_name = syn::LitStr::new(&format!("{}{}", prefix_legacy, hash), position);
let used_attr = if false {
quote! { #[used] }
} else {
quote! {}
};
quote! {
// We bundle both the legacy and new link sections for compatibility.
static __LEGACY_LINK_SECTION: &'static [u8] = {
const __BUFFER: #buffer_type_legacy = #serialize_fn_legacy(&#item);
const __BYTES: &[u8] = __BUFFER.as_ref();
const __LEN: usize = __BYTES.len();
#used_attr
#[unsafe(export_name = #legacy_export_name)]
static __LINK_SECTION: [u8; __LEN] = #copy_bytes_fn(__BYTES);
&__LINK_SECTION
};
static __LINK_SECTION: &'static [u8] = {
const __BUFFER: #buffer_type_current = #serialize_fn_current(&#item);
const __BYTES: &[u8] = __BUFFER.as_ref();
const __LEN: usize = __BYTES.len();
#used_attr
#[unsafe(export_name = #export_name)]
static __LINK_SECTION: [u8; __LEN] = #copy_bytes_fn(__BYTES);
&__LINK_SECTION
};
}
+11
View File
@@ -16,8 +16,19 @@ keywords = ["assets"]
const-serialize = { workspace = true }
manganis-core = { workspace = true }
manganis-macro = { workspace = true }
thiserror = { workspace = true }
const-serialize-07 = { package = "const-serialize", version = "=0.7.2" }
[target.'cfg(target_os = "android")'.dependencies]
jni = "0.21"
ndk-context = "0.1.1"
[target.'cfg(target_os = "ios")'.dependencies]
objc2 = "0.6.3"
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6.3"
[features]
default = []
dioxus = ["manganis-core/dioxus"]
+18 -14
View File
@@ -1,23 +1,17 @@
# Manganis
The Manganis allows you to submit assets to any build tool that supports collecting assets. It makes it easy to self-host assets that are distributed throughout your libraries. Manganis also handles optimizing, converting, and fetching assets.
Manganis is a tool for submitting assets and native source files to the program linker. It makes it easy to self-host assets and native plugins that are distributed throughout your libraries.
If you defined this in a component library:
## Assets
Including assets is as simple as using the `asset!()` macro:
```rust
use manganis::{Asset, asset};
const AVIF_ASSET: Asset = manganis::asset!("/assets/image.png");
const STYLE: Asset = asset!("/assets/style.css");
```
AVIF_ASSET will be set to a new file name that will be served by some CLI. That file can be collected by any package that depends on the component library.
If you have assets that may not always be bundled, you can fall back gracefully with `option_asset!`:
```rust
use manganis::{Asset, asset, option_asset};
const REQUIRED: Asset = asset!("/assets/style.css");
const OPTIONAL: Option<Asset> = option_asset!("/assets/missing.css");
```
After cargo builds your app, the asset path is embedded directly in the data section. A tool like the Dioxus CLI can extract this metadata and post-process these assets.
```rust
use manganis::{ImageFormat, Asset, asset, ImageSize, AssetOptions};
@@ -34,6 +28,16 @@ pub const RESIZED_PNG_ASSET: Asset =
pub const AVIF_ASSET: Asset = asset!("/assets/image.png", AssetOptions::image().with_format(ImageFormat::Avif));
```
## Adding Support to Your CLI
## option_asset
To add support for your CLI, you need to integrate with the [manganis_cli_support](https://github.com/DioxusLabs/manganis/tree/main/cli-support) crate. This crate provides utilities to collect assets that integrate with the Manganis macro. It makes it easy to integrate an asset collection and optimization system into a build tool.
If you have assets that may not always be bundled, you can fall back gracefully with `option_asset!`:
```rust
use manganis::{Asset, asset, option_asset};
const REQUIRED: Asset = asset!("/assets/style.css");
const OPTIONAL: Option<Asset> = option_asset!("/assets/missing.css");
```
## Native FFI Bindings
Manganis provides the `#[ffi]` attribute macro for generating direct FFI bindings between Rust and native platforms (Swift/Kotlin). See the `geolocation-native-plugin` example for usage.
@@ -0,0 +1,63 @@
use jni::{objects::JObject, JNIEnv, JavaVM};
use std::sync::OnceLock;
/// Cached reference to the Android activity.
static ACTIVITY: OnceLock<jni::objects::GlobalRef> = OnceLock::new();
static JAVA_VM: OnceLock<JavaVM> = OnceLock::new();
/// Execute a JNI operation with a cached activity reference.
///
/// This function handles the boilerplate of getting the JavaVM and Activity
/// references, caching them for subsequent calls. It's the foundation for
/// most Android mobile API operations.
///
/// # Arguments
///
/// * `f` - A closure that receives a mutable JNIEnv and the Activity JObject
///
/// # Returns
///
/// Returns `Some(R)` if the operation succeeds, `None` if there's an error
/// getting the VM or Activity references.
///
/// # Example
///
/// ```rust,no_run
/// use dioxus_platform_bridge::android::with_activity;
///
/// let result = with_activity(|env, activity| {
/// // Your JNI operations here
/// Some(42)
/// });
/// ```
pub fn with_activity<F, R>(f: F) -> Option<R>
where
F: FnOnce(&mut JNIEnv<'_>, &JObject<'_>) -> Option<R>,
{
let ctx = ndk_context::android_context();
let vm = if let Some(vm) = JAVA_VM.get() {
vm
} else {
let raw_vm = unsafe { JavaVM::from_raw(ctx.vm().cast()) }.ok()?;
let _ = JAVA_VM.set(raw_vm);
JAVA_VM.get()?
};
let mut env = vm.attach_current_thread().ok()?;
let activity = if let Some(activity) = ACTIVITY.get() {
activity
} else {
let raw_activity = unsafe { JObject::from_raw(ctx.context() as jni::sys::jobject) };
let global = env.new_global_ref(&raw_activity).ok()?;
match ACTIVITY.set(global) {
Ok(()) => ACTIVITY.get().unwrap(),
Err(global) => {
drop(global);
ACTIVITY.get()?
}
}
};
let activity_obj = activity.as_obj();
f(&mut env, &activity_obj)
}
@@ -0,0 +1,132 @@
use jni::{
objects::{GlobalRef, JClass, JObject, JValue},
sys::jlong,
JNIEnv, NativeMethod,
};
use crate::android::java::Result;
/// Generic callback system for loading DEX classes and registering native methods
pub struct CallbackSystem {
bytecode: &'static [u8],
class_name: &'static str,
callback_name: &'static str,
callback_signature: &'static str,
}
impl CallbackSystem {
/// Create a new callback system
///
/// # Arguments
///
/// * `bytecode` - The compiled DEX bytecode
/// * `class_name` - The fully qualified Java class name
/// * `callback_name` - The name of the native callback method
/// * `callback_signature` - The JNI signature of the callback method
pub fn new(
bytecode: &'static [u8],
class_name: &'static str,
callback_name: &'static str,
callback_signature: &'static str,
) -> Self {
Self {
bytecode,
class_name,
callback_name,
callback_signature,
}
}
/// Load the DEX class and register the native callback method
///
/// This function handles the boilerplate of:
/// 1. Creating an InMemoryDexClassLoader
/// 2. Loading the specified class
/// 3. Registering the native callback method
///
/// # Returns
///
/// Returns a `GlobalRef` to the loaded class, or an error if loading fails
pub fn load_and_register(&self, env: &mut JNIEnv<'_>) -> Result<GlobalRef> {
let callback_class = self.load_dex_class(env)?;
self.register_native_callback(env, &callback_class)?;
let global = env.new_global_ref(callback_class)?;
Ok(global)
}
/// Load the DEX class from bytecode
fn load_dex_class<'a>(&self, env: &mut JNIEnv<'a>) -> Result<JClass<'a>> {
const IN_MEMORY_LOADER: &str = "dalvik/system/InMemoryDexClassLoader";
// Create a ByteBuffer from our DEX bytecode
let byte_buffer = unsafe {
env.new_direct_byte_buffer(self.bytecode.as_ptr() as *mut u8, self.bytecode.len())
}?;
// Create an InMemoryDexClassLoader with our DEX bytecode
let dex_class_loader = env.new_object(
IN_MEMORY_LOADER,
"(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V",
&[
JValue::Object(&byte_buffer),
JValue::Object(&JObject::null()),
],
)?;
// Load our class
let class_name = env.new_string(self.class_name)?;
let class = env
.call_method(
&dex_class_loader,
"loadClass",
"(Ljava/lang/String;)Ljava/lang/Class;",
&[JValue::Object(&class_name)],
)?
.l()?;
Ok(class.into())
}
/// Register the native callback method with the Java class
fn register_native_callback<'a>(
&self,
env: &mut JNIEnv<'a>,
callback_class: &JClass<'a>,
) -> Result<()> {
env.register_native_methods(
callback_class,
&[NativeMethod {
name: self.callback_name.into(),
sig: self.callback_signature.into(),
fn_ptr: rust_callback as *mut _,
}],
)?;
Ok(())
}
}
/// Generic native callback function called from Java
///
/// SAFETY: This function is called from Java and must maintain proper memory safety.
/// The handler pointer is valid as long as the Manager exists (see Drop implementation).
#[no_mangle]
unsafe extern "C" fn rust_callback<'a>(
mut env: JNIEnv<'a>,
_class: JObject<'a>,
handler_ptr_high: jlong,
handler_ptr_low: jlong,
location: JObject<'a>,
) {
// Reconstruct the pointer from two i64 values (for 64-bit pointers)
#[cfg(not(target_pointer_width = "64"))]
compile_error!("Only 64-bit Android targets are supported");
let handler_ptr_raw: usize =
((handler_ptr_high as u64) << 32 | handler_ptr_low as u64) as usize;
// Convert to our callback function pointer
let callback: fn(&mut JNIEnv, JObject) = unsafe { std::mem::transmute(handler_ptr_raw) };
// Use the local reference for this JNI frame; avoid leaking a global ref.
callback(&mut env, location);
}
@@ -0,0 +1,127 @@
use jni::{
objects::{JClass, JObject, JObjectArray, JString, JValue, JValueOwned},
JNIEnv,
};
/// Result type for JNI operations
pub type Result<T> = std::result::Result<T, jni::errors::Error>;
/// Helper functions for common JNI operations
/// Create a new Java string tied to the current JNI frame
pub fn new_string<'env>(env: &mut JNIEnv<'env>, s: &str) -> Result<JString<'env>> {
env.new_string(s)
}
/// Create a new object array
pub fn new_object_array<'env>(
env: &mut JNIEnv<'env>,
len: i32,
element_class: &str,
) -> Result<JObjectArray<'env>> {
env.new_object_array(len, element_class, &JObject::null())
}
/// Set an element in an object array
pub fn set_object_array_element<'env>(
env: &mut JNIEnv<'env>,
array: &JObjectArray<'env>,
index: i32,
element: JString<'env>,
) -> Result<()> {
env.set_object_array_element(array, index, element)
}
/// Call a static method on a class
pub fn call_static_method<'env, 'obj>(
env: &mut JNIEnv<'env>,
class: &JClass<'env>,
method_name: &str,
signature: &str,
args: &[JValue<'env, 'obj>],
) -> Result<JValueOwned<'env>> {
env.call_static_method(class, method_name, signature, args)
}
/// Call an instance method on an object
pub fn call_method<'env, 'obj>(
env: &mut JNIEnv<'env>,
obj: &JObject<'env>,
method_name: &str,
signature: &str,
args: &[JValue<'env, 'obj>],
) -> Result<JValueOwned<'env>> {
env.call_method(obj, method_name, signature, args)
}
/// Find a Java class by name
pub fn find_class<'env>(env: &mut JNIEnv<'env>, class_name: &str) -> Result<JClass<'env>> {
env.find_class(class_name)
}
/// Create a new object instance
pub fn new_object<'env, 'obj>(
env: &mut JNIEnv<'env>,
class_name: &str,
signature: &str,
args: &[JValue<'env, 'obj>],
) -> Result<JObject<'env>> {
env.new_object(class_name, signature, args)
}
/// Check if a permission is granted (Activity.checkSelfPermission)
pub fn check_self_permission(
env: &mut JNIEnv,
activity: &JObject,
permission: &str,
) -> Result<bool> {
let permission_string = new_string(env, permission)?;
let status = env.call_method(
activity,
"checkSelfPermission",
"(Ljava/lang/String;)I",
&[JValue::Object(&permission_string)],
)?;
const PERMISSION_GRANTED: i32 = 0;
Ok(status.i()? == PERMISSION_GRANTED)
}
/// Request permissions via a helper class's static method
///
/// This uses PermissionsHelper.requestPermissionsOnUiThread(pattern)
/// to request permissions on the UI thread.
pub fn request_permissions_via_helper(
env: &mut JNIEnv,
helper_class: &JClass,
activity: &JObject,
permissions: JObjectArray,
request_code: i32,
) -> Result<()> {
env.call_static_method(
helper_class,
"requestPermissionsOnUiThread",
"(Landroid/app/Activity;[Ljava/lang/String;I)V",
&[
JValue::Object(activity),
JValue::Object(&permissions.into()),
JValue::Int(request_code),
],
)?;
Ok(())
}
/// Load a Java class from the APK's classloader
pub fn load_class_from_classloader<'env>(
env: &mut JNIEnv<'env>,
class_name: &str,
) -> Result<JClass<'env>> {
let class_name_jstring = new_string(env, class_name)?;
let class = env.call_static_method(
"java/lang/Class",
"forName",
"(Ljava/lang/String;)Ljava/lang/Class;",
&[JValue::Object(&class_name_jstring)],
)?;
Ok(class.l()?.into())
}
@@ -0,0 +1,9 @@
//! Android metadata wrappers for linker-based collection.
pub use manganis_core::AndroidArtifactMetadata;
pub type AndroidMetadataBuffer = crate::macro_helpers::ConstVec<u8, 4096>;
pub const fn serialize_android_metadata(meta: &AndroidArtifactMetadata) -> AndroidMetadataBuffer {
crate::macro_helpers::serialize_android_artifact(meta)
}
@@ -0,0 +1,39 @@
//! Android-specific utilities for mobile APIs
#[cfg(target_os = "android")]
pub mod activity;
#[cfg(target_os = "android")]
pub mod callback;
#[cfg(target_os = "android")]
pub mod java;
#[cfg(target_os = "android")]
pub mod metadata;
#[doc(hidden)]
pub mod macro_helpers {
//! Helper functions for macro expansion
//!
//! These functions are used internally by the `android_plugin!()` macro
//! and should not be used directly.
/// Copy a slice into a constant sized buffer at compile time
pub const fn copy_bytes<const N: usize>(bytes: &[u8]) -> [u8; N] {
let mut out = [0; N];
let mut i = 0;
while i < N {
out[i] = bytes[i];
i += 1;
}
out
}
}
#[cfg(target_os = "android")]
pub use activity::*;
#[cfg(target_os = "android")]
pub use callback::*;
#[cfg(target_os = "android")]
pub use java::*;
#[cfg(target_os = "android")]
pub use metadata::*;
@@ -0,0 +1,29 @@
//! Darwin (iOS/macOS) shared utilities for objc2-based APIs
//!
//! This module provides shared utilities for both iOS and macOS platforms
//! since they share the same Objective-C runtime and threading requirements
//! through objc2.
/// Re-export MainThreadMarker for convenience
pub use objc2::MainThreadMarker;
/// Macro helpers for FFI code generation
pub mod macro_helpers {
pub use crate::macro_helpers::copy_bytes;
}
pub use manganis_core::SwiftPackageMetadata;
pub mod metadata {
use manganis_core::SwiftPackageMetadata;
/// Buffer type for serialized Swift metadata
#[doc(hidden)]
pub type SwiftMetadataBuffer = crate::macro_helpers::ConstVec<u8, 4096>;
/// Serialize Swift package metadata for linker embedding
#[doc(hidden)]
pub const fn serialize_swift_metadata(meta: &SwiftPackageMetadata) -> SwiftMetadataBuffer {
crate::macro_helpers::serialize_swift_package(meta)
}
}
+52 -3
View File
@@ -1,5 +1,4 @@
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#[doc(hidden)]
pub mod macro_helpers;
@@ -8,6 +7,56 @@ pub use manganis_macro::css_module;
pub use manganis_macro::option_asset;
pub use manganis_core::{
Asset, AssetOptions, AssetVariant, BundledAsset, CssAssetOptions, CssModuleAssetOptions,
FolderAssetOptions, ImageAssetOptions, ImageFormat, ImageSize, JsAssetOptions,
// Core asset types
Asset,
AssetOptions,
AssetVariant,
BundledAsset,
// Standard asset options
CssAssetOptions,
CssModuleAssetOptions,
FolderAssetOptions,
ImageAssetOptions,
ImageFormat,
ImageSize,
JsAssetOptions,
};
// Re-export metadata types for FFI and sidecar macros
pub use manganis_core::SwiftPackageMetadata;
pub use manganis_core::SymbolData;
// FFI utilities and plugin metadata for Dioxus mobile platform APIs
//
// This crate provides common patterns and utilities for implementing
// mobile platform APIs in Dioxus applications. It handles the
// boilerplate for JNI (Android) and objc2 (iOS/macOS) bindings, build scripts,
// and platform-specific resource management.
/// Android platform utilities
#[doc(hidden)]
pub mod android;
/// Darwin (iOS/macOS) platform utilities
#[doc(hidden)]
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub mod darwin;
#[cfg(target_os = "android")]
pub use android::*;
// Export darwin module for iOS, macOS, and when metadata feature is enabled (for FFI macro)
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub use darwin::*;
/// Re-export commonly used types for convenience
#[cfg(target_os = "android")]
pub use jni;
// Re-export objc2 for FFI macro generated code
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub use objc2;
/// Re-export the ffi attribute macro for native FFI bindings
/// This macro generates direct FFI bindings between Rust and native platforms (Swift/Kotlin)
pub use manganis_macro::ffi;
+120 -28
View File
@@ -1,11 +1,44 @@
// Re-export const_serialize types for generated code.
pub use const_serialize;
use const_serialize::{serialize_const, ConstVec, SerializeConst};
pub use const_serialize::{ConstStr, ConstVec, SerializeConst};
pub use const_serialize_07;
use const_serialize_07::{
serialize_const as serialize_const_07, ConstVec as ConstVec07,
SerializeConst as SerializeConst07,
use const_serialize_07::ConstVec as ConstVec07;
use manganis_core::{
AndroidArtifactMetadata, AssetOptions, BundledAsset, SwiftPackageMetadata, SymbolData,
};
use manganis_core::{AssetOptions, BundledAsset};
/// Copy a slice into a constant sized buffer at compile time
///
/// This is a generic utility that works with any byte slice and can be used
/// in const contexts to create fixed-size arrays from dynamic slices.
pub const fn copy_bytes<const N: usize>(bytes: &[u8]) -> [u8; N] {
let mut out = [0; N];
let mut i = 0;
while i < N {
out[i] = bytes[i];
i += 1;
}
out
}
/// Serialize a SymbolData value into a const buffer
///
/// This is used by the widget!() macro and other symbol-based macros to embed
/// metadata into the binary using the 4096-byte buffer format.
pub const fn serialize_symbol_data(symbol_data: &SymbolData) -> ConstVec<u8, 4096> {
dx_macro_helpers::serialize_to_const_with_max_padded::<4096>(symbol_data)
}
/// Serialize Android artifact metadata (wrapped in `SymbolData::AndroidArtifact`).
pub const fn serialize_android_artifact(meta: &AndroidArtifactMetadata) -> ConstVec<u8, 4096> {
serialize_symbol_data(&SymbolData::AndroidArtifact(*meta))
}
/// Serialize Swift package metadata (wrapped in `SymbolData::SwiftPackage`).
pub const fn serialize_swift_package(meta: &SwiftPackageMetadata) -> ConstVec<u8, 4096> {
serialize_symbol_data(&SymbolData::SwiftPackage(*meta))
}
/// Create a bundled asset from the input path, the content hash, and the asset options
pub const fn create_bundled_asset(input_path: &str, asset_config: AssetOptions) -> BundledAsset {
@@ -26,25 +59,16 @@ pub const fn create_bundled_asset_relative(
}
/// Serialize an asset to a const buffer
pub const fn serialize_asset(asset: &BundledAsset) -> ConstVec<u8> {
let data = ConstVec::new();
let mut data = serialize_const(asset, data);
// Reserve the maximum size of the asset
while data.len() < <BundledAsset as SerializeConst>::MEMORY_LAYOUT.size() {
data = data.push(0);
}
data
///
/// Serializes the asset directly (not wrapped in SymbolData) for simplicity.
/// Uses a 4096-byte buffer and pads to the full size to match linker section size.
pub const fn serialize_asset(asset: &BundledAsset) -> ConstVec<u8, 4096> {
dx_macro_helpers::serialize_to_const_with_max_padded::<4096>(asset)
}
/// Serialize an asset to a const buffer in the legacy 0.7 format
pub const fn serialize_asset_07(asset: &BundledAsset) -> ConstVec07<u8> {
let data = ConstVec07::new();
let mut data = serialize_const_07(asset, data);
// Reserve the maximum size of the asset
while data.len() < <BundledAsset as SerializeConst07>::MEMORY_LAYOUT.size() {
data = data.push(0);
}
data
dx_macro_helpers::serialize_to_const_with_layout_padded_07(asset)
}
/// Deserialize a const buffer into a BundledAsset
@@ -55,13 +79,81 @@ pub const fn deserialize_asset(bytes: &[u8]) -> BundledAsset {
}
}
/// Copy a slice into a constant sized buffer at compile time
pub const fn copy_bytes<const N: usize>(bytes: &[u8]) -> [u8; N] {
let mut out = [0; N];
let mut i = 0;
while i < N {
out[i] = bytes[i];
i += 1;
pub mod dx_macro_helpers {
use const_serialize::{ConstVec, SerializeConst};
/// Serialize a value to a const buffer, padding to the specified size
///
/// This is a generic helper that works with any type implementing `SerializeConst`.
/// It serializes the value and then pads the buffer to the specified memory layout size.
pub const fn serialize_to_const<T: SerializeConst>(
value: &T,
memory_layout_size: usize,
) -> ConstVec<u8> {
let data = ConstVec::new();
let mut data = const_serialize::serialize_const(value, data);
// Reserve the maximum size of the type
while data.len() < memory_layout_size {
data = data.push(0);
}
data
}
/// Serialize a value to a const buffer with a fixed maximum size, padding to the specified size
///
/// This variant uses a `ConstVec` with a fixed maximum size (e.g., `ConstVec<u8, 4096>`)
/// and then pads to the specified memory layout size.
///
/// This function serializes directly into the larger buffer to avoid overflow issues
/// when the serialized data exceeds the default 1024-byte buffer size.
pub const fn serialize_to_const_with_max<const MAX_SIZE: usize>(
value: &impl SerializeConst,
memory_layout_size: usize,
) -> ConstVec<u8, MAX_SIZE> {
// Serialize using the default buffer, then copy into the larger buffer
let serialized = const_serialize::serialize_const(value, ConstVec::new());
let mut data: ConstVec<u8, MAX_SIZE> = ConstVec::new_with_max_size();
data = data.extend(serialized.as_ref());
// Reserve the maximum size of the type (pad to MEMORY_LAYOUT size)
while data.len() < memory_layout_size {
data = data.push(0);
}
data
}
/// Serialize a value to a const buffer and pad to the full buffer size
///
/// This is useful for linker section generation that expects a fixed-size buffer.
pub const fn serialize_to_const_with_max_padded<const MAX_SIZE: usize>(
value: &impl SerializeConst,
) -> ConstVec<u8, MAX_SIZE> {
let serialized = const_serialize::serialize_const(value, ConstVec::new());
let mut data: ConstVec<u8, MAX_SIZE> = ConstVec::new_with_max_size();
data = data.extend(serialized.as_ref());
while data.len() < MAX_SIZE {
data = data.push(0);
}
data
}
/// Serialize a value using the legacy 0.7 const-serialize format and pad to layout size
///
/// Note: The legacy ConstVec has a 1024-byte limit. If MEMORY_LAYOUT.size() exceeds this,
/// we pad only up to the buffer limit to avoid overflow.
pub const fn serialize_to_const_with_layout_padded_07<T: const_serialize_07::SerializeConst>(
value: &T,
) -> const_serialize_07::ConstVec<u8> {
let data = const_serialize_07::ConstVec::new();
let mut data = const_serialize_07::serialize_const(value, data);
// Pad to MEMORY_LAYOUT size, but cap at 1024 bytes (the legacy buffer limit)
let target_size = if T::MEMORY_LAYOUT.size() > 1024 {
1024
} else {
T::MEMORY_LAYOUT.size()
};
while data.len() < target_size {
data = data.push(0);
}
data
}
out
}