diff --git a/Cargo.toml b/Cargo.toml index eda17fbc..4d52e7fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,9 @@ members = [ "fs-properties", "fs-index", "fs-storage", - "dev-hash", + "dev-hash", + "rpc", + "rpc_example", ] default-members = [ @@ -32,3 +34,4 @@ default-members = [ ] resolver = "2" + \ No newline at end of file diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml new file mode 100644 index 00000000..d2481e54 --- /dev/null +++ b/rpc/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rpc" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["lib"] +name = "rpc" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.121" +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "1.0", features = ["full"] } +once_cell = "1.19.0" +uniffi = "0.28.1" diff --git a/rpc/README.md b/rpc/README.md new file mode 100644 index 00000000..07f73ef9 --- /dev/null +++ b/rpc/README.md @@ -0,0 +1,30 @@ +## Requirements + +cargo install cargo-ndk +rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android + +## Usage Guide + +### Go to Rust Directory + +```sh +cd rust/ +``` + +### Build the dylib + +```sh +cargo build +``` + +### Build the Android libraries in jniLibs + +```sh +cargo ndk -o ../android/app/src/main/jniLibs --manifest-path ./Cargo.toml -t armeabi-v7a -t arm64-v8a -t x86 -t x86_64 build --release +``` + +### Create Kotlin bindings + +```sh +cargo run --bin uniffi-bingen generate --library ./target/debug/rpc_core.dll --language kotlin --out-dir ../android/app/src/main/java/ark/rpc_core +``` diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs new file mode 100644 index 00000000..1b3c6273 --- /dev/null +++ b/rpc/src/lib.rs @@ -0,0 +1,25 @@ +pub mod router; +pub use once_cell; + +#[macro_export] +macro_rules! uniffi_rpc_server { + ($($name:ident),*) => { + pub static ROUTER: rpc::once_cell::sync::Lazy = rpc::once_cell::sync::Lazy::new(|| { + let mut router = Router::new(); + $( + router.add(stringify!($name), $name); + )* + router + }); + + #[uniffi::export] + pub fn call(path: String, data: Vec) -> String { + ROUTER.call(&path, data) + } + + #[uniffi::export] + pub async fn call_async(path:String,data:Vec) -> String { + ROUTER.call(&path,data) + } + }; +} diff --git a/rpc/src/router.rs b/rpc/src/router.rs new file mode 100644 index 00000000..af028e4b --- /dev/null +++ b/rpc/src/router.rs @@ -0,0 +1,153 @@ +use std::{collections::HashMap, marker::PhantomData}; + +use serde::{Deserialize, Serialize}; + +pub struct Router { + pub routes: HashMap>, +} + +impl Router { + pub fn new() -> Self { + Router { + routes: HashMap::new(), + } + } + + pub fn from_routes( + routes: HashMap>, + ) -> Self { + Router { routes } + } + + pub fn add( + &mut self, + name: &str, + function: impl HandlerFunction, + ) { + self.routes.insert( + name.to_owned(), + Box::new(FunctionHandler { + function, + marker: PhantomData, + }), + ); + } + + pub fn call(&self, name: &str, args: Vec) -> String { + match self.routes.get(name) { + Some(handler) => handler.call(args), + None => NOT_FOUND.into(), + } + } +} + +impl Default for Router { + fn default() -> Self { + Router::new() + } +} + +#[derive(Serialize)] +pub struct Response { + pub result: Option, + pub error: Option, + pub is_success: bool, +} + +const CATASTROPHIC_ERROR: &str = "{\"result\": null, \"error\": \"CATASTROPHIC_ERROR: Failed to serialize response\", \"is_success\": false}"; +const NOT_FOUND: &str = "{\"result\": null, \"error\": \"NOT_FOUND: Unknown function\", \"is_success\": false}"; + +impl Response { + pub fn success(result: T) -> Self { + Response { + result: Some(result), + error: None, + is_success: true, + } + } + + pub fn error(error: String) -> Self { + Response { + result: None, + error: Some(error), + is_success: false, + } + } +} +pub trait HandlerFunction: Send + Sync + 'static { + fn call(&self, args: Vec) -> String; +} + +#[allow(non_snake_case)] +impl HandlerFunction R> for F +where + F: Fn(T0) -> R + Send + Sync + 'static, + T0: for<'a> Deserialize<'a>, + R: Serialize, +{ + fn call(&self, args: Vec) -> String { + let response = { + let mut args = args.into_iter(); + let T0 = serde_json::from_str::( + &args.next().unwrap_or("{}".to_string()), + ); + match T0 { + core::result::Result::Ok(T0) => Response::success((self)(T0)), + _ => Response::error( + "Failed to deserialize arguments".to_string(), + ), + } + }; + serde_json::to_string(&response).unwrap_or(CATASTROPHIC_ERROR.into()) + } +} + +macro_rules! impl_handler_function { + ($($type:ident),+) => { + #[allow(non_snake_case)] + impl HandlerFunction R> for F + where + F: Fn($($type),+) -> R + Send + Sync + 'static, + $($type: for<'a> Deserialize<'a>,)+ + R: Serialize, + { + fn call(&self, args: Vec) -> String { + let response = { + let mut args = args.into_iter(); + let ($($type,)*) = ( + $( + serde_json::from_str::<$type>(&args.next().unwrap_or("{}".to_string())) + ),+ + ); + match ($($type,)*) { + ($(core::result::Result::Ok($type),)*) => Response::success((self)($($type,)*)), + _ => Response::error(format!("Failed to deserialize arguments")), + } + }; + serde_json::to_string(&response).unwrap_or(CATASTROPHIC_ERROR.into()) + } + } + }; +} + +impl_handler_function!(T0, T1); +impl_handler_function!(T0, T1, T2); +impl_handler_function!(T0, T1, T2, T3); +impl_handler_function!(T0, T1, T2, T3, T4); + +struct FunctionHandler { + function: F, + marker: PhantomData, +} + +impl, Marker> Handler + for FunctionHandler +{ + fn call(&self, args: Vec) -> String { + self.function.call(args) + } +} + +pub trait Handler { + fn call(&self, args: Vec) -> String; +} diff --git a/rpc_example/.gitignore b/rpc_example/.gitignore new file mode 100644 index 00000000..addc62a7 --- /dev/null +++ b/rpc_example/.gitignore @@ -0,0 +1,3 @@ +/jniLibs +/bindings +/kotlin \ No newline at end of file diff --git a/rpc_example/Cargo.toml b/rpc_example/Cargo.toml new file mode 100644 index 00000000..51ed987d --- /dev/null +++ b/rpc_example/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "rpc_example" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +name = "rpc_example" + +[[bin]] +name = "uniffi-bindgen" +path = "uniffi-bindgen.rs" + +[dependencies] +uniffi = { version = "0.28.1", features = [ "cli" ] } +rpc = { path = "../rpc" } + + diff --git a/rpc_example/README.md b/rpc_example/README.md new file mode 100644 index 00000000..a29185a8 --- /dev/null +++ b/rpc_example/README.md @@ -0,0 +1,24 @@ +## Requirements + +cargo install cargo-ndk +rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android + +## Usage Guide + +### Build the dylib + +```sh +cargo build -p rpc_example --release +``` + +### Build the Android libraries in jniLibs + +```sh +cargo ndk -o ./rpc_example/jniLibs --manifest-path ./rpc_example/Cargo.toml -t armeabi-v7a -t arm64-v8a -t x86 -t x86_64 build --release +``` + +### Create Kotlin bindings + +```sh +cargo run -p rpc_example --features=uniffi/cli --bin uniffi-bindgen generate --library ./target/release/rpc_example.dll --language kotlin --out-dir ./rpc_example/bindings +``` \ No newline at end of file diff --git a/rpc_example/scripts/build_kotlin.py b/rpc_example/scripts/build_kotlin.py new file mode 100644 index 00000000..d2793c04 --- /dev/null +++ b/rpc_example/scripts/build_kotlin.py @@ -0,0 +1,140 @@ +import os +import platform +import subprocess +import shutil +import sys +import time + + +# Constants +KOTLINC = "kotlinc.bat" +CLASSPATH = os.getenv('CLASSPATH', './rpc_example/kotlin/vendor/jna.jar;./rpc_example/kotlin/vendor/kotlinx-coroutines.jar') # Fetch CLASSPATH from the environment +LIB_NAME = "rpc_example" +TARGET_DIR = "./target/release" +KOTLIN_OUT_DIR = "./rpc_example/kotlin" + +def run_command(command, print_only=False): + """Run a command in the shell.""" + print(f"[COMMAND] {' '.join(command)}") + if print_only: + return + + return subprocess.run(command, shell=True) + +def get_lib_extension(): + """Determine the correct library extension based on the operating system.""" + current_os = platform.system().lower() + if current_os == "darwin": + return "dylib" + elif current_os == "linux": + return "so" + elif current_os == "windows": + return "dll" + else: + sys.exit("Unknown OS. Supported OS: mac, linux, windows.") + +def build_library(): + """Build the Rust library using cargo.""" + print("[INFO] Building library...") + + result = run_command(["cargo", "build", "-p", "rpc_example", "--release"]) + if result.returncode != 0: + sys.exit("Failed to build library") + +def generate_binding(lib_extension): + """Generate Kotlin bindings using UniFFI.""" + print("[INFO] Generating Kotlin binding...") + kotlin_lib_path = os.path.join(KOTLIN_OUT_DIR, LIB_NAME) + if os.path.exists(kotlin_lib_path): + shutil.rmtree(kotlin_lib_path) + + run_command(["cargo", "run", "-p", "rpc_example", "--features=uniffi/cli", "--bin", "uniffi-bindgen", "generate", + "--library", f"{TARGET_DIR}/{LIB_NAME}.{lib_extension}", + "--language", "kotlin", "--out-dir", KOTLIN_OUT_DIR]) + + +def copy_cdylib(lib_extension): + """Copy the built cdylib to the Kotlin output directory.""" + print("[INFO] Copying cdylib to output directory...") + src = f"{TARGET_DIR}/{LIB_NAME}.{lib_extension}" + dest = f"{KOTLIN_OUT_DIR}/{LIB_NAME}.{lib_extension}" + + print(f"[COMMAND] cp {src} {dest}") + shutil.copy(src, dest) + +def build_jar(): + """Compile Kotlin files into a JAR.""" + print("[INFO] Building Kotlin JAR...") + jar_file = f"{KOTLIN_OUT_DIR}/rpc_example.jar" + if os.path.exists(jar_file): + os.remove(jar_file) + + run_command( + [KOTLINC, "-Werror", "-d", jar_file, f"{KOTLIN_OUT_DIR}/uniffi/rpc_example/rpc_example.kt","-classpath",f'"{CLASSPATH}"'], True) + +def run_tests(): + """Run Kotlin script tests.""" + print("[INFO] Executing tests...") + test_names = ["blocking"] + for test_name in test_names: + print(f"[INFO] Running {test_name}_test.kts ...") + run_command( + [KOTLINC, "-Werror", "-J-ea", "-classpath", + f'"{CLASSPATH};{KOTLIN_OUT_DIR}/rpc_example.jar;{KOTLIN_OUT_DIR}"', + "-script", f"./rpc_example/tests/{test_name}_test.kts"], + True + ) + +def dependencies(): + vendor_folder = f"{KOTLIN_OUT_DIR}/vendor" + + if not os.path.exists(vendor_folder): + os.makedirs(vendor_folder) + + # download jna.jar if it doesn't exist + + + jna_url = "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar" + jna_file = f"{vendor_folder}/jna.jar" + if os.path.exists(jna_file): + print(f"[INFO] JNA already exists at {jna_file}") + else: + run_command(["curl", "-L", jna_url, "-o", jna_file]) + + # download kotlinx-coroutines.jar + kotlinx_coroutines_url = "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.6.4/kotlinx-coroutines-core-jvm-1.6.4.jar" + kotlinx_coroutines_file = f"{vendor_folder}/kotlinx-coroutines.jar" + if os.path.exists(kotlinx_coroutines_file): + print(f"[INFO] kotlinx-coroutines already exists at {kotlinx_coroutines_file}") + else: + run_command(["curl", "-L", kotlinx_coroutines_url, "-o", kotlinx_coroutines_file]) + + +def main(): + lib_extension = get_lib_extension() + + # Build library + build_library() + + # Generate Kotlin bindings + generate_binding(lib_extension) + + # Copy cdylib + copy_cdylib(lib_extension) + + # Get 3rd party dependencies + dependencies(); + + # Build the Kotlin jar + build_jar() + + # # Execute Kotlin tests + run_tests() + +if __name__ == "__main__": + try: + main() + except subprocess.CalledProcessError as e: + sys.exit(f"[ERROR] Command failed: {e}") + except Exception as e: + sys.exit(f"[ERROR] An error occurred: {e}") diff --git a/rpc_example/src/lib.rs b/rpc_example/src/lib.rs new file mode 100644 index 00000000..83aefcc7 --- /dev/null +++ b/rpc_example/src/lib.rs @@ -0,0 +1,19 @@ +use rpc::{router::Router, uniffi_rpc_server}; + +uniffi::setup_scaffolding!(); + +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +pub fn add_array(left: u64, array: Vec) -> u64 { + let a: u64 = array.iter().sum(); + return a + left; +} + +pub fn add_with_wait(left: u64, right: u64) -> u64 { + std::thread::sleep(std::time::Duration::from_secs(5)); + left + right +} + +uniffi_rpc_server!(add, add_array, add_with_wait); diff --git a/rpc_example/tests/blocking_test.kts b/rpc_example/tests/blocking_test.kts new file mode 100644 index 00000000..e30de584 --- /dev/null +++ b/rpc_example/tests/blocking_test.kts @@ -0,0 +1,38 @@ +import uniffi.* +import kotlinx.coroutines.* +import kotlin.system.* + +kotlinx.coroutines.runBlocking { + // function factorial does not exist + var factorial = uniffi.rpc_example.call("factorial", listOf("10")) + assert(factorial == "{\"result\": null, \"error\": \"NOT_FOUND: Unknown function\", \"is_success\": false}") + + // testing single argument function + var add = uniffi.rpc_example.call("add", listOf("10", "20")) + assert(add == "{\"result\":30,\"error\":null,\"is_success\":true}") + + // testing multiple argument function + var add_array = uniffi.rpc_example.call("add_array", listOf("10", "[1,2,3,4,5,6]")) + assert(add_array == "{\"result\":31,\"error\":null,\"is_success\":true}") + + val time = measureTimeMillis { + val one = uniffi.rpc_example.call("add_with_wait", listOf("10", "20")) + val two = uniffi.rpc_example.call("add_with_wait", listOf("10", "20")) + println("The answer is ${one + two}") + } + println("Completed sync in $time ms") + + + val time2 = measureTimeMillis { + println("Starting async test") + val one = async { uniffi.rpc_example.callAsync("add_with_wait", listOf("10", "20")) } + println("First async call") + val two = async { uniffi.rpc_example.callAsync("add_with_wait", listOf("10", "20")) } + println("The answer is ${one.await() + two.await()}") + } + println("Completed async in $time2 ms") + + + println("Blocking Test Passed") + +} \ No newline at end of file diff --git a/rpc_example/uniffi-bindgen.rs b/rpc_example/uniffi-bindgen.rs new file mode 100644 index 00000000..f6cff6cf --- /dev/null +++ b/rpc_example/uniffi-bindgen.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::uniffi_bindgen_main() +}