Skip to main content
Version: 0.3

Rust Setup for Android/iOS Bindings

This tutorial guides you through building iOS and Android bindings from a Rust project. It’s divided into three sections:

If you want to quickly get started with Circom .wasm and .zkey files, refer to the

info

Make sure you've installed the prerequisites.


🌎 General setup​

0. Initialize a new project​

If you already have a Rust project, skip ahead to 1. Add dependencies.

If you don’t have an existing Rust project, start by initializing a new Rust crate with creating a new directory (e.g., mopro-rust-project) and change into it:

mkdir mopro-rust-project
cd mopro-rust-project

Initialize a new Rust crate by running:

cargo init --lib

1. Add dependencies​

Include the crate in your Cargo.toml

Cargo.toml
[features]
default = ["uniffi"]
uniffi = ["mopro-ffi/uniffi"]
flutter = ["mopro-ffi/flutter"]

[dependencies]
mopro-ffi = "0.3"
thiserror = "2.0.12"

[build-dependencies]
mopro-ffi = "0.3"

2. Setup the lib​

Define the crate-type for the UniFFI build process configuration.

Cargo.toml
[lib]
crate-type = ["lib", "cdylib", "staticlib"]

3. Use mopro-ffi macro​

The mopro-ffi macro exports several FFI configurations. To enable it, activate the mopro-ffi macro in src/lib.rs.

src/lib.rs
mopro_ffi::app!();

4. Define the binaries​

The binaries are used to generate bindings for both iOS and Android platforms.

We'll add a new file at src/bin/ios.rs:

src/bin/ios.rs
fn main() {
mopro_ffi::app_config::ios::build();
}

and another at src/bin/android.rs:

src/bin/android.rs
fn main() {
mopro_ffi::app_config::android::build();
}

You can also apply this to Flutter and React Native.

src/bin/react_native.rs
fn main() {
mopro_ffi::app_config::react_native::build();
}
src/bin/flutter.rs
fn main() {
mopro_ffi::app_config::flutter::build();
}

5. What's next​

You have two options moving forward:

  • Integrate a supported prover – Choose one of the built-in options: Circom, Halo2, or Noir (see the next section).
  • Customize your bindings – If you're using a different prover or want to expose your own business logic, skip ahead to the Customize Bindings section.

πŸ” Integrating a ZK prover​

Be sure to complete the general setup before continuing.

This tutorial will cover:

  1. Circom prover
  2. Halo2 prover
  3. Noir prover
info

If you’re starting from scratch, the setup process is very similar to using the mopro CLI. Please refer to the Getting Started guide for detailed instructions.

Setup Circom-Based rust project​

πŸ“’ Ensure you've completed the General Setup before proceeding.

1. Add witness generator​

Each witness generator must be built within a project. You need to supply the required data for it to generate a circuit-specific execution function.

Here, we used rust-witness as an example.

info

To learn more about witnesscalc and circom-witnesscalc for Mopro, please check out circom-prover.

Include the rust-witness in your Cargo.toml and add circom-prover in Cargo.toml:

Cargo.toml
[dependencies]
circom-prover = "0.1"
rust-witness = "0.1"
num-bigint = "0.4.0"

[build-dependencies]
rust-witness = "0.1"

In build.rs, add the following code to compile the witness generator wasm sources (.wasm) into a native library and link to it:

build.rs
fn main() {
rust_witness::transpile::transpile_wasm("../path to directory containing your wasm sources");
// e.g. rust_witness::transpile::transpile_wasm("./test-vectors".to_string());
// The directory should contain the following files:
// - <circuit name>.wasm
}
info

Learn more about .wasm files in Circom documentation.
Here are the example WASM and Zkey files to be downloaded.

2. Add the Helper Template​

Use the Circom helper template by adding a src/circom.rs file based on this example: circom.rs. This template exposes the Circom prover interface through FFIs and makes it available to mobile native packages.

And add an error handler src/error.rs for the mobile native bindings based on this example: error.rs.

Enable the circom and error module in src/lib.rs

src/lib.rs
mod error;
pub use error::MoproError;
mod circom;
pub use circom::{
generate_circom_proof, verify_circom_proof, CircomProof, CircomProofResult, ProofLib, G1, G2,
};

Then, use the witness function to associate it with a .zkey file. This allows the proof generation function to know which witness function to use when generating the proof.

src/lib.rs
// Activate rust-witness function
mod witness {
rust_witness::witness!(multiplier2);
}

// Set the witness functions to a zkey
set_circom_circuits! {
(
"multiplier2_final.zkey",
circom_prover::witness::WitnessFn::RustWitness(witness::multiplier2_witness)
),
}
info

To ensure the circuit is correctly integrated with Rust, consider writing unit tests to verify the bindings.

src/lib.rs
#[cfg(test)]
mod circom_tests {
use super::*;

#[test]
fn test_multiplier2() {
let zkey_path = "./test-vectors/circom/multiplier2_final.zkey".to_string();
let circuit_inputs = "{\"a\": 2, \"b\": 3}".to_string();
let result = generate_circom_proof(zkey_path.clone(), circuit_inputs, ProofLib::Arkworks);
assert!(result.is_ok());
let proof = result.unwrap();
let valid = verify_circom_proof(zkey_path, proof, ProofLib::Arkworks);
assert!(valid.is_ok());
assert!(valid.unwrap());
}
}

3. Generate bindings for iOS and Android​

Now you're ready to build your static library! You should be able to run either the Mopro CLI or the binaries.

1. Execute the process through Mopro CLI (recommended πŸ‘πŸ»)

To install the Mopro CLI, please refer to the Getting Started guide.

Execute the building through

mopro build

Then, you can select the target mode and architectures with greater flexibility.

2. Execute the process using the defined binaries.

For example:

cargo run --bin ios # Debug mode for iOS
cargo run --bin android # Debug mode for Android
CONFIGURATION=release cargo run --bin ios # Release mode for iOS
CONFIGURATION=release cargo run --bin android # Release mode for Android
ANDROID_ARCHS=x86_64-linux-android cargo run --bin android # Build for Android x86_64-linux-android architecture
IOS_ARCHS=aarch64-apple-ios,aarch64-apple-ios-sim cargo run --bin ios # Build for iOS aarch64-apple-ios and aarch64-apple-ios-sim architecture

to build the corresponding static library.

info

Running your project in release mode significantly enhances performance compared to debug mode. This is because the Rust compiler applies optimizations that improve runtime speed and reduce binary size, making your application more efficient.

4. What's next​

Once the bindings are successfully built, you will see the MoproiOSBindings, MoproAndroidBindings, MoproReactNativeBindings and/or mopro_flutter_bindings folders.

Next, you have two options:

  • Use the mopro create command from the mopro CLI to generate ready-to-use templates for your desired framework (e.g., Swift, Kotlin, React Native, or Flutter).
  • If you already have a mobile app or prefer to manually integrate the bindings, follow the iOS Setup, Android Setup, React Native Setup and/or FLutter Setup sections.
  • If you find that some functionality is still missing on mobile, you can refer to the Customize the Bindings section to learn how to expose additional functions using any Rust crate.
  • After making changes, be sure to run:
    mopro build
    mopro update
    This ensures the bindings are regenerated and reflect your latest updates.
warning

If you haven’t integrated other adapters and encounter an error like:

Cannot find 'generateHalo2Proof' in scope

you can fix it by adding the stubs.rs file in src/stubs.rs and activating the stubs in src/lib.rs:

src/lib.rs
#[macro_use]
mod stubs;

// Activate stubs for adapters that are not integrated
halo2_stub!();
noir_stub!();

This ensures that missing adapter functions are stubbed, preventing compilation errors while keeping your project runnable.


Setup Halo2-Based rust project​

πŸ“’ Ensure you've completed the General Setup before proceeding.

1. Add Halo2 circuits​

Import the Halo2 prover as a Rust crate using:

Cargo.toml
[dependencies]
plonk-fibonacci = { package = "plonk-fibonacci", git = "https://github.com/sifnoc/plonkish-fibonacci-sample.git" }
anyhow = "1.0.99"
info

See how to define a Halo2 prover crate and generate SRS here: plonkish-fibonacci-sample

Next, copy your SRS and key files into the project folder. For this tutorial, we'll assume you place them in test-vectors/halo2.

info

Download example SRS and key files :

Now, add three key files, for example, in test-vectors/halo2 folder.

2. Add the Helper Template​

Use the Halo2 helper template by adding a src/halo2.rs file based on this example: halo2.rs. This template exposes the Halo2 prover interface through FFIs and makes it available to mobile native packages.

And add an error handler src/error.rs for the mobile native bindings based on this example: error.rs.

Enable the halo2 and error module in src/lib.rs

src/lib.rs
mod error;
pub use error::MoproError;
mod halo2;
pub use halo2::{generate_halo2_proof, verify_halo2_proof, Halo2ProofResult};

Then, associate each key file with its corresponding execution functions. This ensures the proof generation function knows which function to use when generating a proof.

src/lib.rs
set_halo2_circuits! {
("plonk_fibonacci_pk.bin", plonk_fibonacci::prove, "plonk_fibonacci_vk.bin", plonk_fibonacci::verify),
}
info

To ensure the circuit is correctly integrated with Rust, consider writing unit tests to verify the bindings.

src/lib.rs
#[cfg(test)]
mod halo2_tests {
use std::collections::HashMap;
use super::*;

#[test]
fn test_plonk_fibonacci() {
let srs_path = "./test-vectors/halo2/plonk_fibonacci_srs.bin".to_string();
let pk_path = "./test-vectors/halo2/plonk_fibonacci_pk.bin".to_string();
let vk_path = "./test-vectors/halo2/plonk_fibonacci_vk.bin".to_string();
let mut circuit_inputs = HashMap::new();
circuit_inputs.insert("out".to_string(), vec!["55".to_string()]);
let result = generate_halo2_proof(srs_path.clone(), pk_path.clone(), circuit_inputs);
assert!(result.is_ok());
let halo2_proof_result = result.unwrap();
let valid = verify_halo2_proof(
srs_path,
vk_path,
halo2_proof_result.proof,
halo2_proof_result.inputs,
);
assert!(valid.is_ok());
assert!(valid.unwrap());
}
}

3. Generate bindings for iOS and Android​

Similar to Circom-based Rust project setup 3. Generate bindings for iOS and Android

4. What's next​

Similar to Circom-based Rust project setup 4. What's next


Setup Noir-Based rust project​

πŸ“’ Ensure you've completed the General Setup before proceeding.

0. Prepare Noir circuits​

Follow the Noir documentation to build your circuit. You’ll need to generate a .json file from the compiled circuit for use in this project.

Downloading the SRS (Structured Reference String) is optional but recommended, as it can significantly improve proving performance. See Downloading SRS for more details.

1. Add noir prover​

Cargo.toml
[dependencies]
noir_rs = { package = "noir", git = "https://github.com/zkmopro/noir-rs", features = [
"barretenberg",
"android-compat",
], branch = "v1.0.0-beta.8-3" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.94"

[dev-dependencies]
serial_test = "3.0.0"
warning

Noir and its dependencies are not yet published as crates on crates.io. Therefore, we can only import them directly from GitHub.

You’re now ready to generate a Noir proof using mopro-ffi.

info

Download example SRS and circuit files :

And add them to test-vectors/noir/ folder.

Use the Noir helper template by adding a src/noir.rs file based on this example: noir.rs. This template exposes the Noir prover interface through FFIs and makes it available to mobile native packages.

And add an error handler src/error.rs for the mobile native bindings based on this example: error.rs.

Enable the noir and error module in src/lib.rs

src/lib.rs
mod error;
pub use error::MoproError;
mod noir;
pub use noir::{generate_noir_proof, get_noir_verification_key, verify_noir_proof};
info

To verify everything is working correctly, you can write a Rust unit test like the one below to ensure the proof is computed successfully.

src/lib.rs
#[cfg(test)]
mod noir_tests {
use super::*;

#[test]
fn test_noir_multiplier2() {
let srs_path = "./test-vectors/noir/noir_multiplier2.srs".to_string();
let circuit_path = "./test-vectors/noir/noir_multiplier2.json".to_string();
let circuit_inputs = vec!["3".to_string(), "5".to_string()];
let vk = get_noir_verification_key(
circuit_path.clone(),
Some(srs_path.clone()),
true, // on_chain (uses Keccak for Solidity compatibility)
false, // low_memory_mode
)
.unwrap();

let proof = generate_noir_proof(
circuit_path.clone(),
Some(srs_path.clone()),
circuit_inputs.clone(),
true, // on_chain (uses Keccak for Solidity compatibility)
vk.clone(),
false, // low_memory_mode
)
.unwrap();

let valid = verify_noir_proof(
circuit_path,
proof,
true, // on_chain (uses Keccak for Solidity compatibility)
vk,
false, // low_memory_mode
)
.unwrap();
assert!(valid);
}
}

2. Generate bindings for iOS and Android​

Similar to Circom-based Rust project setup 3. Generate bindings for iOS and Android

3. What's next​

Similar to Circom-based Rust project setup 4. What's next


πŸ¦„ Customize the bindings​

πŸ“’ Ensure you've completed the General Setup before proceeding.

If your ZK prover isn’t included, or you already have your own Rust crate, you can use the #[uniffi::export] procedural macro to define custom functions and generate mobile-native bindings from them. For Flutter bindings, you can instead make the function pub in src/lib.rs to expose it.

1. Define exported functions​

Export Rust functions using a procedural macro, as shown below:

src/lib.rs
mopro_ffi::app!(); // Enable the mopro-ffi macro to setup FFI configuration.

// for all bindings
#[cfg_attr(feature = "uniffi", uniffi::export)]
pub fn hello_world() -> String {
"Hello, World!".to_string()
}

// for iOS, Android, and React Native bindings
#[uniffi::export]
pub fn hello_world() -> String {
"Hello, World!".to_string()
}

// for Flutter bindings
pub fn hello_world() -> String {
"Hello, World!".to_string()
}
info

For more examples and detailed references, check the UniFFI documentation.

You may also bring in other Rust crates to extend functionality.

For instance, to use semaphore-protocol, you can add it like this:

Cargo.toml
[dependencies]
semaphore-protocol = { version = "0.1", features = ["serde"] }
src/lib.rs
mopro_ffi::app!();

use semaphore::group::Group;
use semaphore::identity::Identity;
use semaphore::proof::GroupOrMerkleProof;
use semaphore::proof::Proof;
use semaphore::proof::SemaphoreProof;

#[cfg_attr(feature = "uniffi", uniffi::export)]
pub fn semaphore_prove(
id_secret: String,
leaves: Vec<Vec<u8>>,
message: String,
scope: String,
tree_depth: u16,
) -> String {
let identity = Identity::new(id_secret.as_bytes());
let group_members: Vec<[u8; 32]> = leaves
.iter()
.map(|leaf| {
leaf.as_slice()
.try_into()
.expect("Leaf must be exactly 32 bytes")
})
.collect::<Vec<[u8; 32]>>();
let group = Group::new(&group_members).unwrap();
let proof = Proof::generate_proof(
identity,
GroupOrMerkleProof::Group(group),
message.to_string(),
scope.to_string(),
tree_depth,
)
.unwrap();
let proof_json = proof.export().unwrap();
return proof_json;
}

#[cfg_attr(feature = "uniffi", uniffi::export)]
pub fn semaphore_verify(proof: String) -> bool {
let proof = SemaphoreProof::import(&proof).unwrap();
let valid = Proof::verify_proof(proof);
valid
}
Example usage
#[cfg(test)]
mod uniffi_tests {
use super::*;
use semaphore::utils::to_element;

#[test]
fn test_mopro_uniffi_hello_world() {
let secret1 = "secret1";
let secret2 = "secret2";
let identity1 = Identity::new(secret1.as_bytes());
let identity2 = Identity::new(secret2.as_bytes());
let leaves = vec![
to_element(*identity1.commitment()).to_vec(),
to_element(*identity2.commitment()).to_vec(),
];
let message = "message";
let scope = "scope";
let tree_depth = 10;
let proof = semaphore_prove(
secret1.to_string(),
leaves,
message.to_string(),
scope.to_string(),
tree_depth,
);
assert!(semaphore_verify(proof));
}
}

2. Generate bindings for iOS and Android​

Similar to Circom-based Rust project setup 3. Generate bindings for iOS and Android

Once the bindings are generated, you'll see your exported functions (e.g., semaphoreProve, semaphoreVerify) included in the generated codeβ€”for example

  • MoproiOSBindings/mopro.swift for iOS
  • MoproAndroidBindings/uniffi/mopro/mopro.kt for Android
  • MoproReactNativeBindings/src/generated/rust_example.ts for React Native
  • mopro_flutter_bindings/lib/src/rust/third_party/rust_example.dart for Flutter

You can then use these functions directly within your iOS, Android, React Native and/or Flutter applications as part of the generated bindings.

πŸ“‹ Generate Circom bindings​

Install the CLI with Cargo:

cargo install mopro-cli

Install the latest CLI from GitHub:

git clone https://github.com/zkmopro/mopro
cd mopro/cli
cargo install --path .

Use mopro bindgen to specify your circuit path and target architectures (e.g., iOS or Android):

  • Specify configuration directly in the CLI command
mopro bindgen
  • Specify circuit directory
mopro bindgen --circuit-dir ./test-vectors/circom
  • Specify circuit directory, build mode, and architectures
mopro bindgen \
--circuit-dir ./test-vectors/circom \
--mode release \
--platforms ios \
--architectures aarch64-apple-ios-sim
  • View all available options:
mopro bindgen --help
warning

Currently, only the rust-witness, witnesscalc-adapter witness generator is supported.

3. What's next​

Similar to Circom-based Rust project setup 4. What's next