Single/Common core code for WASM, Android and iOS

After reading many resources, and doing my checks, trials and surely Q/As in this forum, I was able to get the below hello world single code that can be compiled to different platforms (WASM, Android and iOS).

Your comments/notes/suggestions about the below are welcomed

use wasm_bindgen::prelude::*;
use std::os::raw::{c_char};
use std::ffi::{CString, CStr};

#[no_mangle]
pub extern fn rust_greeting(to: *const c_char) -> *mut c_char {
    let c_str = unsafe { CStr::from_ptr(to) };
    let recipient = match c_str.to_str() {
        Err(_) => "there",
        Ok(string) => string,
    };
    CString::new("Hello ".to_owned() + recipient).unwrap().into_raw()
}

#[cfg(target_arch = "wasm32")]
pub mod wasm {
    use super::*;

    #[wasm_bindgen]
    pub fn wasm_greetings(to: &str) -> String {
        // copy the &str into CSring
        let c_str = CString::new(to).unwrap();
        // cast the CString into c_char
        let c_world: *const c_char = c_str.as_ptr() as *const c_char;
        // pass the c_char into the required function
        let returned = rust_greeting(c_world);

        // Do required manipulation

        // copy the c_char into CStr
        let c_str = unsafe { CStr::from_ptr(returned) };
        // cast the CStr into &str
        let recipient = match c_str.to_str() {
            Err(_) => "Error",
            Ok(string) => string,
        };

        // cast the &str into String and return it
        recipient.to_string()
    }
}

// For Manual memory cleaning in iOS
#[cfg(target_os="ios")]
#[no_mangle]
pub mod ios {
    use super::*;
    pub extern fn rust_greeting_free(s: *mut c_char) {
        unsafe {
            if s.is_null() { return }
            CString::from_raw(s)
        };
    }
}

/// Expose the JNI interface for android below
#[cfg(target_os="android")]
#[allow(non_snake_case)]
pub mod android {
    extern crate jni;

    use super::*;
    use self::jni::JNIEnv;
    use self::jni::objects::{JClass, JString};
    use self::jni::sys::{jstring};

    #[no_mangle]
    pub unsafe extern fn Java_hasan_RustGreetings_greeting(env: JNIEnv, _: JClass, java_pattern: JString) -> jstring {
        // Our Java companion code might pass-in "world" as a string, hence the name.
        let world = rust_greeting(env.get_string(java_pattern).expect("invalid pattern string").as_ptr());
        // Retake pointer so that we can use it below and allow memory to be freed when it goes out of scope.
        let world_ptr = CString::from_raw(world);
        let output = env.new_string(world_ptr.to_str().unwrap()).expect("Couldn't create java string!");

        output.into_inner();
    }
}

The cargo.toml is:

[package]
name = "greetings"
version = "0.1.0"
authors = ["Hasan Yousef"]
edition = "2018"

[dependencies]

[target.'cfg(target_arch="wasm32")'.dependencies]
wasm-bindgen = "0.2.29"

[target.'cfg(target_os="android")'.dependencies]
jni = { version = "0.5", default-features = false }

[lib]
name = "greetings"
crate-type = ["rlib", "cdylib", "dylib", "staticlib"]
# Android: dylib
# iOS: cdylib [armv7s-apple-ios] and staticlib [Others]
# WASM: cdylib
8 Likes

If you're going for idiomatic rust, I'd recommend having rust_greeting take &str and return String, then do the wrapping for retrieving and returning *mut c_char in a separate function. Maybe the "rustic" function would take Option<&str> for the case where conversion fails?

Then the wasm version doesn't have the additional overhead of converting back and forth. In general, I prefer doing this so the "common core" code is kept separate from the conversion code for various things (including C).

Only other thing is I'm not entirely sure why rust_greeting is #[no_mangle]? The android/ios/wasm functions are each individually exported so the code they call into shouldn't need to be no_mangle itself.

Besides that, it looks great! I like the idea of making something like this and this is an awesome proof-of-concept.

3 Likes

Thanks, actually the code I started work with was build initially for iOS, then Android and WASM had bee added, so the rust_greeting is mo_mangle to fit with iOS.
Your comment is absolutely correct, I've to re-write it as clean/version (if the term is valid here), then do the required conversion in each mod think the overall code will be cleaner.

I re-wrote it based on your advise as below, any comment :wink:

I tested both WASMand Android and they are fine.

src folder tree:

Hasans-Air:src h_ajsf$ tree .
.
├── lib.rs
├── android
│   └── mod.rs
├── ios
│   └── mod.rs
└── wasm
    └── mod.rs

3 directories, 4 files

And the files now are:

Main:

// lib.rs
pub mod wasm;
pub mod ios;
pub mod android;

#[cfg(not(target_arch = "wasm32"))]
use std::os::raw::{c_char};

#[cfg(not(target_arch = "wasm32"))]
use std::ffi::{CStr};

pub fn rust_greetings(to: &str) -> String {
    format!("Hello {}", to)
}

#[cfg(not(target_arch = "wasm32"))]
fn char_str(pattern: *const c_char) -> &'static str {

    let c_str = unsafe { CStr::from_ptr(pattern) };
    let string_ptr = match c_str.to_str() {
        Err(_) => "there",
        Ok(string) => string,
    };
    string_ptr
}

WASM

// wasm/mod.rs
#[cfg(target_arch = "wasm32")]
pub mod wasm {
    use crate::rust_greetings;
    use wasm_bindgen::prelude::*;

    #[wasm_bindgen]
    pub fn wasm_greetings(to: &str) -> String {
        rust_greetings(to)
    }
}

Android

// android/mod.rs
#[cfg(target_os="android")]
#[allow(non_snake_case)]
pub mod android {
    use crate::rust_greetings;
    use crate::char_str;
    extern crate jni;

    use self::jni::JNIEnv;
    use self::jni::objects::{JClass, JString};
    use self::jni::sys::{jstring};

    #[no_mangle]
    pub unsafe extern fn Java_com_hasan_RustGreetings_greetings(env: JNIEnv, _: JClass, java_pattern: JString) -> jstring {

        let jvm_input = env.get_string(java_pattern)
                                        .expect("invalid pattern string").as_ptr();

        let input = rust_greetings(char_str(jvm_input));

        let output = env.new_string(input)
                                        .expect("Couldn't create java string!");
        output.into_inner()
    }
}

iOS

// ios/mod.rs
#[cfg(target_os="ios")]
#[no_mangle]
pub mod ios {
    use crate::char_str;

    use std::ffi::{CString, CStr};
    use std::os::raw::{c_char};
    use crate::rust_greetings;

    #[no_mangle]
    pub extern fn ios_greetings(to: *const c_char) -> *mut c_char {

        let input = rust_greetings(char_str(to));

        CString::new(input).unwrap().into_raw()
    }

    pub extern fn iso_greeting_free(s: *mut c_char) {
        unsafe {
            if s.is_null() { return }
            CString::from_raw(s)
        };
    }
}

TOML

# Cargo.toml
[package]
name = "greetings"
version = "0.1.0"
authors = ["Hasan Yousef"]
edition = "2018"

[dependencies]

[target.'cfg(target_arch="wasm32")'.dependencies]
wasm-bindgen = "0.2.29"

[target.'cfg(target_os="android")'.dependencies]
jni = { version = "0.5", default-features = false }

[lib]
name = "rust_greetings"
crate-type = ["cdylib", "dylib", "staticlib"] 
# Android: dylib
# iOS: cdylib [armv7s-apple-ios] and staticlib [Others]
# WASM: cdylib
5 Likes

@hyousef thanks so much for writing this up! This is super useful!

2 Likes

I have developed a tool for developers to build mobile applications in Rust.
https://github.com/sidneywang/rsbind

1 Like