diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml new file mode 100644 index 000000000000..3e4b44f47dd6 --- /dev/null +++ b/.github/workflows/build-wasm.yml @@ -0,0 +1,72 @@ +name: build wasm + +on: [push] + +jobs: + linux: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - name: Set up rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - name: Build with rust + run: | + cd ${{ github.workspace }}/shenyu-wasm/shenyu-wasm-build + /home/runner/.cargo/bin/cargo clean + /home/runner/.cargo/bin/cargo build --release + - uses: actions/upload-artifact@v2 + with: + name: lib + path: shenyu-wasm/shenyu-wasm-build/target/release/lib* + if-no-files-found: error + + mac: + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - name: Set up rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - name: Build with rust + run: | + cd ${{ github.workspace }}/shenyu-wasm/shenyu-wasm-build + /Users/runner/.cargo/bin/cargo clean + /Users/runner/.cargo/bin/cargo build --release + - uses: actions/upload-artifact@v2 + with: + name: lib + path: shenyu-wasm/shenyu-wasm-build/target/release/lib* + if-no-files-found: error + + windows: + runs-on: windows-2019 + steps: + - uses: actions/checkout@v2 + - name: Set up rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - name: Build with rust + run: | + cd ${{ github.workspace }}/shenyu-wasm/shenyu-wasm-build + C://Users//runneradmin//.cargo//bin//cargo.exe clean + C://Users//runneradmin//.cargo//bin//cargo.exe build --release + - uses: actions/upload-artifact@v2 + with: + name: lib + path: shenyu-wasm/shenyu-wasm-build/target/release/*.dll + if-no-files-found: error + - uses: actions/upload-artifact@v2 + with: + name: lib + path: shenyu-wasm/shenyu-wasm-build/target/release/*.lib + if-no-files-found: error diff --git a/.gitignore b/.gitignore index f9530016a975..591626208c89 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ Thumbs.db # agent build ignore /agent/ +/**/Cargo.lock diff --git a/pom.xml b/pom.xml index 27d66e5544b8..88a4eb30e89c 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,7 @@ shenyu-sdk shenyu-discovery shenyu-kubernetes-controller + shenyu-wasm diff --git a/shenyu-wasm/pom.xml b/shenyu-wasm/pom.xml new file mode 100644 index 000000000000..d387bb77229e --- /dev/null +++ b/shenyu-wasm/pom.xml @@ -0,0 +1,32 @@ + + + + + + org.apache.shenyu + shenyu + 2.6.0-SNAPSHOT + + 4.0.0 + + shenyu-wasm + pom + + shenyu-wasm-runtime + + \ No newline at end of file diff --git a/shenyu-wasm/shenyu-wasm-build/Cargo.toml b/shenyu-wasm/shenyu-wasm-build/Cargo.toml new file mode 100644 index 000000000000..f37fb7bd15d4 --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-build/Cargo.toml @@ -0,0 +1,15 @@ +[package] +publish = false +name = "shenyu-wasm-build" +version = "0.1.0" +authors = ["zhangzicheng@apache.org"] +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasmer = { git = "https://github.com/wasmerio/wasmer", rev = "1.0.0" } +wasmer-runtime = { git = "https://github.com/wasmerio/wasmer", rev = "1.0.0" } +wasmer-runtime-core = { git = "https://github.com/wasmerio/wasmer", rev = "1.0.0" } +jni = "0.16" diff --git a/shenyu-wasm/shenyu-wasm-build/src/exception.rs b/shenyu-wasm/shenyu-wasm-build/src/exception.rs new file mode 100644 index 000000000000..f77d500948c4 --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-build/src/exception.rs @@ -0,0 +1,44 @@ +pub use jni::errors::Error; +use jni::{errors::ErrorKind, JNIEnv}; +use std::thread; + +pub fn runtime_error(message: String) -> Error { + Error::from_kind(ErrorKind::Msg(message)) +} + +#[derive(Debug)] +pub enum JOption { + Some(T), + None, +} + +impl JOption { + pub fn unwrap_or(self, default: T) -> T { + match self { + JOption::Some(result) => result, + JOption::None => default, + } + } +} + +pub fn joption_or_throw(env: &JNIEnv, result: thread::Result>) -> JOption { + match result { + Ok(result) => match result { + Ok(result) => JOption::Some(result), + Err(error) => { + if !env.exception_check().unwrap() { + env.throw_new("java/lang/RuntimeException", &error.to_string()) + .expect("Cannot throw an `java/lang/RuntimeException` exception."); + } + + JOption::None + } + }, + Err(ref error) => { + env.throw_new("java/lang/RuntimeException", format!("{:?}", error)) + .expect("Cannot throw an `java/lang/RuntimeException` exception."); + + JOption::None + } + } +} diff --git a/shenyu-wasm/shenyu-wasm-build/src/instance.rs b/shenyu-wasm/shenyu-wasm-build/src/instance.rs new file mode 100644 index 000000000000..2d7904fc5c2d --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-build/src/instance.rs @@ -0,0 +1,220 @@ +use crate::{ + exception::{joption_or_throw, runtime_error, Error}, + memory, + memory::Memory, + types::{jptr, Pointer}, + value::{Value, DOUBLE_CLASS, FLOAT_CLASS, INT_CLASS, LONG_CLASS}, +}; +use jni::{ + objects::{GlobalRef, JClass, JObject, JString, JValue}, + sys::{jbyteArray, jobjectArray}, + JNIEnv, +}; +use std::{collections::HashMap, convert::TryFrom, panic, rc::Rc}; +use wasmer_runtime::{imports, instantiate, DynFunc, Export, Value as WasmValue}; +use wasmer_runtime as core; + +pub struct Instance { + pub java_instance_object: GlobalRef, + pub instance: Rc, + pub memories: HashMap, +} + +impl Instance { + fn new(java_instance_object: GlobalRef, module_bytes: Vec) -> Result { + let module_bytes = module_bytes.as_slice(); + let imports = imports! {}; + let instance = match instantiate(module_bytes, &imports) { + Ok(instance) => Rc::new(instance), + Err(e) => { + return Err(runtime_error(format!( + "Failed to instantiate the module: {}", + e + ))) + } + }; + + let memories: HashMap = instance + .exports() + .filter_map(|(export_name, export)| match export { + Export::Memory(memory) => Some((export_name.to_string(), Memory::new(Rc::new(memory.clone())))), + _ => None, + }) + .collect(); + + Ok(Self { + java_instance_object, + instance, + memories, + }) + } + + fn call_exported_function( + &self, + export_name: String, + arguments: Vec, + ) -> Result, Error> { + let function: DynFunc = self.instance.exports.get(&export_name).map_err(|_| { + runtime_error(format!( + "Exported function `{}` does not exist", + export_name + )) + })?; + + function + .call(arguments.as_slice()) + .map_err(|e| runtime_error(format!("{}", e))) + } +} + +#[no_mangle] +pub extern "system" fn Java_org_apache_shenyu_wasmer_Instance_nativeInstantiate( + env: JNIEnv, + _class: JClass, + this: JObject, + module_bytes: jbyteArray, +) -> jptr { + let output = panic::catch_unwind(|| { + let module_bytes = env.convert_byte_array(module_bytes)?; + let java_instance = env.new_global_ref(this)?; + + let instance = Instance::new(java_instance, module_bytes)?; + + Ok(Pointer::new(instance).into()) + }); + + joption_or_throw(&env, output).unwrap_or(0) +} + +#[no_mangle] +pub extern "system" fn Java_org_apache_shenyu_wasmer_Instance_nativeDrop( + _env: JNIEnv, + _class: JClass, + instance_pointer: jptr, +) { + let _: Pointer = instance_pointer.into(); +} + +#[no_mangle] +pub extern "system" fn Java_org_apache_shenyu_wasmer_Instance_nativeCallExportedFunction<'a>( + env: JNIEnv<'a>, + _class: JClass, + instance_pointer: jptr, + export_name: JString, + arguments_pointer: jobjectArray, +) -> jobjectArray { + let output = panic::catch_unwind(|| { + let instance: &Instance = Into::>::into(instance_pointer).borrow(); + let export_name: String = env.get_string(export_name)?.into(); + + let arguments_length = env.get_array_length(arguments_pointer)?; + + let arguments = (0..arguments_length) + .map(|i| env.get_object_array_element(arguments_pointer, i)) + .collect::, Error>>()?; + + let results = instance.call_exported_function( + export_name.clone(), + arguments + .iter() + .enumerate() + .map(|(nth, argument)| { + Ok( + Value::try_from((&env, *argument)) + .map_err(|_| { + runtime_error(format!( + "Failed to convert the argument {}nth of `{}` into a WebAssembly value.", + nth, + export_name, + )) + })? + .inner()) + }) + .collect::, Error>>()?, + )?; + + let obj_array = env.new_object_array( + i32::try_from(results.len()).map_err(|e| runtime_error(e.to_string()))?, + "java/lang/Object", + JObject::null(), + )?; + + if results.len() > 0 { + for (nth, result) in results.iter().enumerate() { + let obj = match result { + WasmValue::I32(val) => env.new_object(INT_CLASS, "(I)V", &[JValue::from(*val)]), + WasmValue::I64(val) => { + env.new_object(LONG_CLASS, "(J)V", &[JValue::from(*val)]) + } + WasmValue::F32(val) => { + env.new_object(FLOAT_CLASS, "(F)V", &[JValue::from(*val)]) + } + WasmValue::F64(val) => { + env.new_object(DOUBLE_CLASS, "(D)V", &[JValue::from(*val)]) + } + _ => unreachable!(), + }?; + + env.set_object_array_element(obj_array, nth as i32, obj)?; + } + + Ok(obj_array) + } else { + Ok(JObject::null().into_inner()) + } + }); + + joption_or_throw(&env, output).unwrap_or(JObject::null().into_inner()) +} + +#[no_mangle] +pub extern "system" fn Java_org_apache_shenyu_wasmer_Instance_nativeInitializeExportedFunctions( + env: JNIEnv, + _class: JClass, + instance_pointer: jptr, +) { + let output = panic::catch_unwind(|| { + let instance: &Instance = Into::>::into(instance_pointer).borrow(); + + let exports_object: JObject = env + .get_field( + instance.java_instance_object.as_obj(), + "exports", + "Lorg/wasmer/Exports;", + )? + .l()?; + + for (export_name, export) in instance.instance.exports() { + if let Export::Function { .. } = export { + let name = env.new_string(export_name)?; + + env.call_method( + exports_object, + "addFunction", + "(Ljava/lang/String;)V", + &[JObject::from(name).into()], + )?; + } + } + Ok(()) + }); + + joption_or_throw(&env, output).unwrap_or(()) +} + +#[no_mangle] +pub extern "system" fn Java_org_apache_shenyu_wasmer_Instance_nativeInitializeExportedMemories( + env: JNIEnv, + _class: JClass, + instance_pointer: jptr, +) { + let output = panic::catch_unwind(|| { + let instance: &Instance = Into::>::into(instance_pointer).borrow(); + + memory::java::initialize_memories(&env, instance)?; + + Ok(()) + }); + + joption_or_throw(&env, output).unwrap_or(()) +} diff --git a/shenyu-wasm/shenyu-wasm-build/src/lib.rs b/shenyu-wasm/shenyu-wasm-build/src/lib.rs new file mode 100644 index 000000000000..1c377baea9cc --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-build/src/lib.rs @@ -0,0 +1,6 @@ +mod exception; +mod instance; +mod memory; +mod module; +mod types; +mod value; diff --git a/shenyu-wasm/shenyu-wasm-build/src/memory.rs b/shenyu-wasm/shenyu-wasm-build/src/memory.rs new file mode 100644 index 000000000000..4346585b247e --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-build/src/memory.rs @@ -0,0 +1,147 @@ +use crate::{ + exception::{joption_or_throw, runtime_error, Error}, + types::{jptr, Pointer}, +}; +use jni::{ + objects::{JClass, JObject}, + sys::jint, + JNIEnv, +}; +use std::{cell::Cell, panic, rc::Rc, slice}; +use wasmer_runtime::memory::MemoryView; +use wasmer_runtime::units::Pages; +use wasmer::Memory as WasmMemory; + +#[derive(Clone)] +pub struct Memory { + pub memory: Rc, +} + +impl Memory { + pub fn new(memory: Rc) -> Self { + Self { memory } + } + + pub fn grow(&self, number_of_pages: u32) -> Result { + self.memory + .grow(Pages(number_of_pages)) + .map(|previous_pages| previous_pages.0) + .map_err(|e| runtime_error(format!("Failed to grow the memory: {}", e))) + } +} + +#[no_mangle] +pub extern "system" fn Java_org_apache_shenyu_wasmer_Memory_nativeMemoryView( + env: JNIEnv, + _class: JClass, + memory_object: JObject, + memory_pointer: jptr, +) { + let output = panic::catch_unwind(|| { + let memory: &Memory = Into::>::into(memory_pointer).borrow(); + let view: MemoryView = memory.memory.view(); + let data = unsafe { + slice::from_raw_parts_mut(view[..].as_ptr() as *mut Cell as *mut u8, view.len()) + }; + + // Create a new `JByteBuffer`, aka `java.nio.ByteBuffer`, + // borrowing the data from the WebAssembly memory. + let byte_buffer = env.new_direct_byte_buffer(data)?; + + // Try to rewrite the `org.wasmer.Memory.buffer` attribute by + // calling the `org.wasmer.Memory.setBuffer` method. + env.call_method( + memory_object, + "setBuffer", + "(Ljava/nio/ByteBuffer;)V", + &[JObject::from(byte_buffer).into()], + )?; + + Ok(()) + }); + + joption_or_throw(&env, output); +} + +#[no_mangle] +pub extern "system" fn Java_org_apache_shenyu_wasmer_Memory_nativeMemoryGrow( + env: JNIEnv, + _class: JClass, + memory_object: JObject, + memory_pointer: jptr, + number_of_pages: jint, +) -> jint { + let output = panic::catch_unwind(|| { + let memory: &Memory = Into::>::into(memory_pointer).borrow(); + let old_pages = memory.grow(number_of_pages as u32)?; + + let view: MemoryView = memory.memory.view(); + let data = unsafe { + std::slice::from_raw_parts_mut( + view[..].as_ptr() as *mut Cell as *mut u8, + view.len(), + ) + }; + // Create a new `JByteBuffer`, aka `java.nio.ByteBuffer`, + // borrowing the data from the WebAssembly memory. + let byte_buffer = env.new_direct_byte_buffer(data)?; + + // Try to rewrite the `org.wasmer.Memory.buffer` attribute by + // calling the `org.wasmer.Memory.setBuffer` method. + env.call_method( + memory_object, + "setBuffer", + "(Ljava/nio/ByteBuffer;)V", + &[JObject::from(byte_buffer).into()], + )?; + + Ok(old_pages as i32) + }); + + joption_or_throw(&env, output).unwrap_or(0) +} + +pub mod java { + use crate::{ + exception::Error, + instance::Instance, + types::{jptr, Pointer}, + }; + use jni::{objects::JObject, JNIEnv}; + + pub fn initialize_memories(env: &JNIEnv, instance: &Instance) -> Result<(), Error> { + let exports_object: JObject = env + .get_field( + instance.java_instance_object.as_obj(), + "exports", + "Lorg/wasmer/Exports;", + )? + .l()?; + + // Get the `org.wasmer.Memory` class. + let memory_class = env.find_class("org/wasmer/Memory")?; + + for (memory_name, memory) in &instance.memories { + // Instantiate the `Memory` class. + let memory_object = env.new_object(memory_class, "()V", &[])?; + + // Try to set the memory pointer to the field `org.wasmer.Memory.memoryPointer`. + let memory_pointer: jptr = Pointer::new(memory.clone()).into(); + env.set_field(memory_object, "memoryPointer", "J", memory_pointer.into())?; + + // Add the newly created `org.wasmer.Memory` in the + // `org.wasmer.Exports` collection. + env.call_method( + exports_object, + "addMemory", + "(Ljava/lang/String;Lorg/wasmer/Memory;)V", + &[ + JObject::from(env.new_string(memory_name)?).into(), + memory_object.into(), + ], + )?; + } + + Ok(()) + } +} diff --git a/shenyu-wasm/shenyu-wasm-build/src/module.rs b/shenyu-wasm/shenyu-wasm-build/src/module.rs new file mode 100644 index 000000000000..a10d7b1e9404 --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-build/src/module.rs @@ -0,0 +1,184 @@ +use crate::{ + exception::{joption_or_throw, runtime_error, Error}, + instance::Instance, + memory::Memory, + types::{jptr, Pointer}, +}; +use jni::{ + objects::{GlobalRef, JClass, JObject}, + sys::{jboolean, jbyteArray}, + JNIEnv, +}; +use std::{collections::HashMap, panic, rc::Rc}; +use wasmer_runtime::{self as runtime, validate, Export}; +use wasmer_runtime::{cache::Artifact, imports, load_cache_with}; + +pub struct Module { + #[allow(unused)] + java_module_object: GlobalRef, + module: runtime::Module, +} + +impl Module { + fn new(java_module_object: GlobalRef, module_bytes: Vec) -> Result { + let module_bytes = module_bytes.as_slice(); + let module = runtime::compile(module_bytes) + .map_err(|e| runtime_error(format!("Failed to compile the module: {}", e)))?; + + Ok(Self { + java_module_object, + module, + }) + } + + fn serialize(&self) -> Result, Error> { + match self.module.cache() { + Ok(artifact) => match artifact.serialize() { + Ok(serialized_artifact) => Ok(serialized_artifact), + Err(_) => { + return Err(runtime_error(format!( + "Failed to serialize the module artifact." + ))) + } + }, + Err(_) => return Err(runtime_error(format!("Failed to get the module artifact."))), + } + } + + fn deserialize( + java_module_object: GlobalRef, + serialized_module: Vec, + ) -> Result { + let module = match unsafe { Artifact::deserialize(serialized_module.as_slice()) } { + Ok(artifact) => { + match load_cache_with(artifact) { + Ok(module) => module, + Err(_) => { + return Err(runtime_error(format!( + "Failed to compile the serialized module." + ))) + } + } + } + Err(_) => return Err(runtime_error(format!("Failed to deserialize the module."))), + }; + + Ok(Self { + java_module_object, + module, + }) + } +} + +#[no_mangle] +pub extern "system" fn Java_org_apache_shenyu_wasmer_Module_nativeModuleInstantiate( + env: JNIEnv, + _class: JClass, + this: JObject, + module_bytes: jbyteArray, +) -> jptr { + let output = panic::catch_unwind(|| { + let module_bytes = env.convert_byte_array(module_bytes)?; + let java_module = env.new_global_ref(this)?; + + let module = Module::new(java_module, module_bytes)?; + + Ok(Pointer::new(module).into()) + }); + + joption_or_throw(&env, output).unwrap_or(0) +} + +#[no_mangle] +pub extern "system" fn Java_org_apache_shenyu_wasmer_Module_nativeDrop( + _env: JNIEnv, + _class: JClass, + module_pointer: jptr, +) { + let _: Pointer = module_pointer.into(); +} + +#[no_mangle] +pub extern "system" fn Java_org_apache_shenyu_wasmer_Module_nativeInstantiate( + env: JNIEnv, + _class: JClass, + module_pointer: jptr, + instance_object: JObject, +) -> jptr { + let output = panic::catch_unwind(|| { + let java_instance_object = env.new_global_ref(instance_object)?; + + let module: &Module = Into::>::into(module_pointer).borrow(); + let import_object = imports! {}; + let instance = module.module.instantiate(&import_object).map_err(|e| { + runtime_error(format!("Failed to instantiate a WebAssembly module: {}", e)) + })?; + + let memories: HashMap = instance + .exports() + .filter_map(|(export_name, export)| match export { + Export::Memory(memory) => Some((export_name.to_string(), Memory::new(Rc::new(memory.clone())))), + _ => None, + }) + .collect(); + + Ok(Pointer::new(Instance { + java_instance_object, + instance: Rc::new(instance), + memories, + }) + .into()) + }); + + joption_or_throw(&env, output).unwrap_or(0) +} + +#[no_mangle] +pub extern "system" fn Java_org_apache_shenyu_wasmer_Module_nativeValidate( + env: JNIEnv, + _class: JClass, + module_bytes: jbyteArray, +) -> jboolean { + let output = panic::catch_unwind(|| { + let module_bytes = env.convert_byte_array(module_bytes)?; + match validate(module_bytes.as_slice()) { + true => Ok(1), + false => Ok(0), + } + }); + + joption_or_throw(&env, output).unwrap_or(0) +} + +#[no_mangle] +pub extern "system" fn Java_org_apache_shenyu_wasmer_Module_nativeSerialize( + env: JNIEnv, + _class: JClass, + module_pointer: jptr, +) -> jbyteArray { + let output = panic::catch_unwind(|| { + let module: &Module = Into::>::into(module_pointer).borrow(); + let serialized_module = module.serialize()?; + let java_serialized_module = env.byte_array_from_slice(serialized_module.as_slice())?; + Ok(java_serialized_module) + }); + + joption_or_throw(&env, output).unwrap_or(JObject::null().into_inner()) +} + +#[no_mangle] +pub extern "system" fn Java_org_apache_shenyu_wasmer_Module_nativeDeserialize( + env: JNIEnv, + _class: JClass, + java_module: JObject, + java_serialized_module: jbyteArray, +) -> jptr { + let output = panic::catch_unwind(|| { + let java_module_object = env.new_global_ref(java_module)?; + let serialized_module = env.convert_byte_array(java_serialized_module)?; + let module = Module::deserialize(java_module_object, serialized_module)?; + Ok(Pointer::new(module).into()) + }); + + joption_or_throw(&env, output).unwrap_or(0) +} diff --git a/shenyu-wasm/shenyu-wasm-build/src/types.rs b/shenyu-wasm/shenyu-wasm-build/src/types.rs new file mode 100644 index 000000000000..33b2d6b60595 --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-build/src/types.rs @@ -0,0 +1,44 @@ +use jni::sys::jlong; +use std::ops::Deref; + +#[allow(non_camel_case_types)] +pub type jptr = jlong; + +#[derive(Debug)] +pub struct Pointer { + value: Box, +} + +impl Pointer { + pub fn new(value: Kind) -> Self { + Pointer { + value: Box::new(value), + } + } + + pub fn borrow<'a>(self) -> &'a mut Kind { + Box::leak(self.value) + } +} + +impl From> for jptr { + fn from(pointer: Pointer) -> Self { + Box::into_raw(pointer.value) as _ + } +} + +impl From for Pointer { + fn from(pointer: jptr) -> Self { + Self { + value: unsafe { Box::from_raw(pointer as *mut Kind) }, + } + } +} + +impl Deref for Pointer { + type Target = Kind; + + fn deref(&self) -> &Self::Target { + &self.value + } +} diff --git a/shenyu-wasm/shenyu-wasm-build/src/value.rs b/shenyu-wasm/shenyu-wasm-build/src/value.rs new file mode 100644 index 000000000000..67adbcde5955 --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-build/src/value.rs @@ -0,0 +1,45 @@ +use crate::exception::{runtime_error, Error}; +use jni::{errors::ErrorKind, objects::JObject, JNIEnv}; +use std::convert::TryFrom; +use wasmer_runtime::Value as WasmValue; + +/// Value wrapping the real WebAssembly value. +pub struct Value(WasmValue); + +impl Value { + pub fn inner(self) -> WasmValue { + self.0 + } +} + +pub const INT_CLASS: &str = "java/lang/Integer"; +pub const LONG_CLASS: &str = "java/lang/Long"; +pub const FLOAT_CLASS: &str = "java/lang/Float"; +pub const DOUBLE_CLASS: &str = "java/lang/Double"; + +impl TryFrom<(&JNIEnv<'_>, JObject<'_>)> for Value { + type Error = Error; + + fn try_from((env, jobject): (&JNIEnv, JObject)) -> Result { + if jobject.is_null() { + return Err(ErrorKind::NullPtr("`try_from` receives a null object").into()); + } + + Ok(Value( + if env.is_instance_of(jobject, INT_CLASS).unwrap_or(false) { + WasmValue::I32(env.call_method(jobject, "intValue", "()I", &[])?.i()?) + } else if env.is_instance_of(jobject, LONG_CLASS).unwrap_or(false) { + WasmValue::I64(env.call_method(jobject, "longValue", "()J", &[])?.j()?) + } else if env.is_instance_of(jobject, FLOAT_CLASS).unwrap_or(false) { + WasmValue::F32(env.call_method(jobject, "floatValue", "()F", &[])?.f()?) + } else if env.is_instance_of(jobject, DOUBLE_CLASS).unwrap_or(false) { + WasmValue::F64(env.call_method(jobject, "doubleValue", "()D", &[])?.d()?) + } else { + return Err(runtime_error(format!( + "Could not convert argument {:?} to a WebAssembly value.", + jobject + ))); + }, + )) + } +} diff --git a/shenyu-wasm/shenyu-wasm-runtime/pom.xml b/shenyu-wasm/shenyu-wasm-runtime/pom.xml new file mode 100644 index 000000000000..44ed1899d8cf --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-runtime/pom.xml @@ -0,0 +1,29 @@ + + + + + + org.apache.shenyu + shenyu + 2.6.0-SNAPSHOT + + 4.0.0 + + shenyu-wasm-runtime + + \ No newline at end of file diff --git a/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Exports.java b/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Exports.java new file mode 100644 index 000000000000..9650b5d24fec --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Exports.java @@ -0,0 +1,91 @@ +package org.apache.shenyu.wasm; + +import java.util.HashMap; +import java.util.Map; +import org.apache.shenyu.wasm.exports.Export; +import org.apache.shenyu.wasm.exports.Function; + +/** + * `Exports` is a Java class that represents the set of WebAssembly exports. + * + * Example: + *
{@code
+ * Instance instance = new Instance(wasmBytes);
+ *
+ * // Get and run an exported function.
+ * Object[] result = instance.exports.getFunction("sum").apply(1, 2);
+ *
+ * // Get, manually downcast, and run an exported function.
+ * Export sum = instance.exports.get("sum");
+ * Object[] result = ((Function) sum).apply(1, 2);
+ * }
+ */ +public class Exports { + private Map inner; + private Instance instance; + + /** + * The constructor instantiates new exported functions. + * + * @param instance Instance object which holds the exports object. + */ + protected Exports(Instance instance) { + this.inner = new HashMap(); + this.instance = instance; + } + + /** + * Return the export with the name `name`. + * + * @param name Name of the export to return. + */ + public Export get(String name) { + return this.inner.get(name); + } + + /** + * Return the export with the name `name` as an exported function. + * + * @param name Name of the exported function. + */ + public Function getFunction(String name) throws ClassCastException { + return (Function) this.inner.get(name); + } + + /** + * Return the export with the name `name` as an exported memory. + * + * @param name Name of the exported memory. + */ + public Memory getMemory(String name) throws ClassCastException { + return (Memory) this.inner.get(name); + } + + /** + * Called by Rust to add a new exported function. + */ + private void addFunction(String name) { + this.inner.put(name, this.generateFunctionWrapper(name)); + } + + /** + * Called by Rust to add a new exported memory. + */ + private void addMemory(String name, Memory memory) { + this.inner.put(name, memory); + } + + /** + * Lambda expression for currying. + * This takes a function name and returns the function to call WebAssembly function. + */ + private java.util.function.Function functionWrapperGenerator = + functionName -> arguments -> this.instance.nativeCallExportedFunction(this.instance.instancePointer, functionName, arguments); + + /** + * Generate the exported function wrapper. + */ + private Function generateFunctionWrapper(String functionName) { + return this.functionWrapperGenerator.apply(functionName); + } +} diff --git a/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Instance.java b/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Instance.java new file mode 100644 index 000000000000..b59d1f90612b --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Instance.java @@ -0,0 +1,74 @@ +package org.apache.shenyu.wasm; + +/** + * `Instance` is a Java class that represents a WebAssembly instance. + * + * Example: + *
{@code
+ * Instance instance = new Instance(wasmBytes);
+ * }
+ */ +public class Instance { + /** + * Native bindings. + */ + static { + if (!Native.LOADED_EMBEDDED_LIBRARY) { + System.loadLibrary("wasmer_jni"); + } + } + private native long nativeInstantiate(Instance self, byte[] moduleBytes) throws RuntimeException; + private native void nativeDrop(long instancePointer); + protected native Object[] nativeCallExportedFunction(long instancePointer, String exportName, Object[] arguments) throws RuntimeException; + protected static native void nativeInitializeExportedFunctions(long instancePointer); + protected static native void nativeInitializeExportedMemories(long instancePointer); + + /** + * All WebAssembly exports. + */ + public final Exports exports; + + /** + The instance pointer. + */ + protected long instancePointer; + + /** + * The constructor instantiates a new WebAssembly instance based on + * WebAssembly bytes. + * + * @param moduleBytes WebAssembly bytes. + */ + public Instance(byte[] moduleBytes) throws RuntimeException { + this.exports = new Exports(this); + + long instancePointer = this.nativeInstantiate(this, moduleBytes); + this.instancePointer = instancePointer; + + nativeInitializeExportedFunctions(instancePointer); + nativeInitializeExportedMemories(instancePointer); + } + + protected Instance() { + this.exports = new Exports(this); + } + + /** + * Delete an instance object pointer. + */ + public void close() { + if (this.instancePointer != 0L) { + this.nativeDrop(this.instancePointer); + this.instancePointer = 0L; + } + } + + /** + * Delete an instance object pointer, which is called by the garbage collector + * before an object is removed from the memory. + */ + @Override + public void finalize() { + this.close(); + } +} diff --git a/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Memory.java b/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Memory.java new file mode 100644 index 000000000000..7da291245dbf --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Memory.java @@ -0,0 +1,75 @@ +package org.apache.shenyu.wasm; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import org.apache.shenyu.wasm.exports.Export; + +/** + * `Memory` is a Java class that represents a WebAssembly memory. + * + * Example: + *
{@code
+ * Instance instance = new Instance(wasmBytes);
+ * Memory memory = instance.exports.getMemory("memory-name");
+ * ByteBuffer memoryBuffer = memory.buffer();
+ *
+ * // Write bytes.
+ * memoryBuffer.position(0);
+ * memoryBuffer.put(new byte[]{1, 2, 3, 4, 5});
+ *
+ * // Read bytes.
+ * byte[] bytes = new byte[5];
+ * memoryBuffer.position(0);
+ * memoryBuffer.get(bytes);
+ * }
+ */ +public class Memory implements Export { + private native void nativeMemoryView(Memory memory, long memoryPointer); + private native int nativeMemoryGrow(Memory memory, long memoryPointer, int page); + + /** + * Represents the actual WebAssembly memory data, borrowed from the runtime (in Rust). + * The `setBuffer` method must be used to set this attribute. + */ + private ByteBuffer buffer; + private long memoryPointer; + + private Memory() { + // This object is instantiated by Rust. + } + + /** + * Return a _new_ direct byte buffer borrowing the memory data. + * + * @return A new direct byte buffer. + */ + public ByteBuffer buffer() { + this.nativeMemoryView(this, this.memoryPointer); + + return this.buffer; + } + + /** + * Set the `ByteBuffer` of this memory. See `Memory.buffer` to learn more. + * + * In addition, this method correctly sets the endianess of the `ByteBuffer`. + */ + private void setBuffer(ByteBuffer buffer) { + this.buffer = buffer; + + // Ensure the endianess matches WebAssemly specification. + if (this.buffer.order() != ByteOrder.LITTLE_ENDIAN) { + this.buffer.order(ByteOrder.LITTLE_ENDIAN); + } + } + + /** + * Grow this memory by the specified number of pages. + * + * @param page The number of pages to grow. 1 page size is 64KiB. + * @return The previous number of pages. + */ + public int grow(int page) { + return this.nativeMemoryGrow(this, this.memoryPointer, page); + } +} diff --git a/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Module.java b/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Module.java new file mode 100644 index 000000000000..51e2a4ac14f3 --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Module.java @@ -0,0 +1,110 @@ +package org.apache.shenyu.wasm; + +/** + * `Module` is a Java class that represents a WebAssembly module. + * + * Example: + *
{@code
+ * boolean isValid = Module.validate(wasmBytes);
+ *
+ * Module module = new Module(wasmBytes);
+ * Instance instance = module.instantiate();
+ * }
+ */ +public class Module { + /** + * Native bindings. + */ + static { + if (!Native.LOADED_EMBEDDED_LIBRARY) { + System.loadLibrary("wasmer_jni"); + } + } + private native long nativeModuleInstantiate(Module self, byte[] moduleBytes) throws RuntimeException; + private native void nativeDrop(long modulePointer); + private native long nativeInstantiate(long modulePointer, Instance instance); + private static native boolean nativeValidate(byte[] moduleBytes); + private native byte[] nativeSerialize(long modulePointer); + private static native long nativeDeserialize(Module module, byte[] serializedBytes); + + private long modulePointer; + + + /** + * Check that given bytes represent a valid WebAssembly module. + * + * @param moduleBytes WebAssembly bytes. + * @return true if, and only if, given bytes are valid as a WebAssembly module. + */ + public static boolean validate(byte[] moduleBytes) { + return Module.nativeValidate(moduleBytes); + } + + /** + * The constructor instantiates a new WebAssembly module based on + * WebAssembly bytes. + * + * @param moduleBytes WebAssembly bytes. + */ + public Module(byte[] moduleBytes) throws RuntimeException { + long modulePointer = this.nativeModuleInstantiate(this, moduleBytes); + this.modulePointer = modulePointer; + } + + private Module() {} + + /** + * Delete a module object pointer. + */ + public void close() { + if (this.modulePointer != 0L) { + this.nativeDrop(this.modulePointer); + this.modulePointer = 0L; + } + } + + /** + * Delete a module object pointer, which is called by the garbage collector + * before an object is removed from the memory. + */ + @Override + public void finalize() { + this.close(); + } + + /** + * Create an instance object based on a module object. + * + * @return Instance object. + */ + public Instance instantiate() { + Instance instance = new Instance(); + long instancePointer = this.nativeInstantiate(this.modulePointer, instance); + instance.instancePointer = instancePointer; + + Instance.nativeInitializeExportedFunctions(instancePointer); + Instance.nativeInitializeExportedMemories(instancePointer); + return instance; + } + + /** + * Create a serialized byte array from a WebAssembly module. + * + * @return Serialized bytes. + */ + public byte[] serialize() { + return this.nativeSerialize(this.modulePointer); + } + + /** + * Create an original Module object from a byte array. + * + * @return Module object. + */ + public static Module deserialize(byte[] serializedBytes) { + Module module = new Module(); + long modulePointer = Module.nativeDeserialize(module, serializedBytes); + module.modulePointer = modulePointer; + return module; + } +} diff --git a/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Native.java b/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Native.java new file mode 100644 index 000000000000..3013f2ce9361 --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/Native.java @@ -0,0 +1,97 @@ +/** + * Code reduced and simplified from zmq integration in Java. See + * https://github.com/zeromq/jzmq/blob/3384ea1c04876426215fe76b5d1aabc58c099ca0/jzmq-jni/src/main/java/org/zeromq/EmbeddedLibraryTools.java. + */ + +package org.apache.shenyu.wasm; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; + +public class Native { + public static final boolean LOADED_EMBEDDED_LIBRARY; + + static { + LOADED_EMBEDDED_LIBRARY = loadEmbeddedLibrary(); + } + + private Native() {} + + public static String getCurrentPlatformIdentifier() { + String osName = System.getProperty("os.name").toLowerCase(); + + if (osName.contains("windows")) { + osName = "windows"; + } else if (osName.contains("mac os x")) { + osName = "darwin"; + } else { + osName = osName.replaceAll("\\s+", "_"); + } + + return osName + "-" + System.getProperty("os.arch"); + } + + private static boolean loadEmbeddedLibrary() { + boolean usingEmbedded = false; + + // attempt to locate embedded native library within JAR at following location: + // /NATIVE/${os.arch}/${os.name}/[libwasmer.so|libwasmer.dylib|wasmer.dll] + String[] libs; + final String libsFromProps = System.getProperty("wasmer-native"); + + if (libsFromProps == null) { + libs = new String[]{"libwasmer_jni.so", "libwasmer_jni.dylib", "wasmer_jni.dll"}; + } else { + libs = libsFromProps.split(","); + } + + StringBuilder url = new StringBuilder(); + url.append("/org/wasmer/native/"); + url.append(getCurrentPlatformIdentifier()).append("/"); + + URL nativeLibraryUrl = null; + + // loop through extensions, stopping after finding first one + for (String lib: libs) { + nativeLibraryUrl = Module.class.getResource(url.toString() + lib); + + if (nativeLibraryUrl != null) { + break; + } + } + + if (nativeLibraryUrl != null) { + // native library found within JAR, extract and load + try { + final File libfile = File.createTempFile("wasmer_jni", ".lib"); + libfile.deleteOnExit(); // just in case + + final InputStream in = nativeLibraryUrl.openStream(); + final OutputStream out = new BufferedOutputStream(new FileOutputStream(libfile)); + + int len = 0; + byte[] buffer = new byte[8192]; + + while ((len = in.read(buffer)) > -1) { + out.write(buffer, 0, len); + } + + out.close(); + in.close(); + System.load(libfile.getAbsolutePath()); + + usingEmbedded = true; + } catch (IOException x) { + // mission failed, do nothing + } + + } + + return usingEmbedded; + } +} diff --git a/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/exports/Export.java b/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/exports/Export.java new file mode 100644 index 000000000000..e7303280f3c3 --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/exports/Export.java @@ -0,0 +1,7 @@ +package org.apache.shenyu.wasm.exports; + +/** + * Represent a WebAssembly instance export. It could be a function, a + * memory etc. + */ +public interface Export {} diff --git a/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/exports/Function.java b/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/exports/Function.java new file mode 100644 index 000000000000..fdb638b9e4bf --- /dev/null +++ b/shenyu-wasm/shenyu-wasm-runtime/src/main/java/org/apache/shenyu/wasm/exports/Function.java @@ -0,0 +1,15 @@ +package org.apache.shenyu.wasm.exports; + +/** + * Functional interface for WebAssembly exported functions, i.e. it + * creates a new type for a closure that mimics a WebAssembly exported + * function. + * + * The apply method takes an arbitrary number of arguments and returns + * an output. + */ +@FunctionalInterface +public interface Function extends Export { + @SuppressWarnings("unchecked") + public Object[] apply(Object... inputs); +}