diff --git a/.gitignore b/.gitignore index f2a45eb..5e3f836 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ /examples/rust-simple/Cargo.lock /examples/rust-simple/build /examples/rust-simple/target +/examples/rust-string/Cargo.lock +/examples/rust-string/build +/examples/rust-string/target diff --git a/build.gradle b/build.gradle index aefe7ca..3a95571 100644 --- a/build.gradle +++ b/build.gradle @@ -60,12 +60,19 @@ project(':examples') { compileOnly project(':compiler') } + ext.rustBuildRelease = true + task rustToWasm(type: Exec) { - commandLine 'cargo', 'build', '--release' + if (rustBuildRelease) { + commandLine 'cargo', 'build', '--release' + } else { + commandLine 'cargo', 'build' + } } ext.wasmFileName = { -> - def wasmFiles = fileTree(dir: 'target/wasm32-unknown-unknown/release', includes: ['*.wasm']).files + def buildType = rustBuildRelease ? 'release' : 'debug' + def wasmFiles = fileTree(dir: "target/wasm32-unknown-unknown/$buildType", includes: ['*.wasm']).files if (wasmFiles.size() != 1) throw new GradleException('Expected single WASM file, got ' + wasmFiles.size()) return wasmFiles.iterator().next() } @@ -111,4 +118,17 @@ project(':examples:rust-simple') { } mainClassName = 'asmble.examples.rustsimple.Main' +} + +project(':examples:rust-string') { + apply plugin: 'application' + ext.wasmCompiledClassName = 'asmble.generated.RustString' + dependencies { + compile files('build/wasm-classes') + } + compileJava { + dependsOn compileWasm + } + + mainClassName = 'asmble.examples.ruststring.Main' } \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..e186001 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,9 @@ +## Examples + +### Rust + +In order of complexity: + +* [rust-simple](rust-simple) +* [rust-string](rust-string) +* rust-regex \ No newline at end of file diff --git a/examples/rust-simple/README.md b/examples/rust-simple/README.md index 5beeedc..a7e9843 100644 --- a/examples/rust-simple/README.md +++ b/examples/rust-simple/README.md @@ -12,7 +12,7 @@ to `asmble.generated.RustSimple` in `build/wasm-classes`. The class is used by To run it yourself, run the following from the root `asmble` dir (assuming you have built the Gradle wrapper described in the root README's "Building and Testing" section): - ./gradlew --no-daemon :examples:rust-simple:run + gradlew --no-daemon :examples:rust-simple:run Yes, this does include Rust's std lib, but it's not that big of a deal (I'm keeping it around because in other examples as part of [issue #9](https://github.com/cretz/asmble/issues/9) I'll need it). The actual method executed for `add_one` diff --git a/examples/rust-string/.cargo/config b/examples/rust-string/.cargo/config new file mode 100644 index 0000000..435ed75 --- /dev/null +++ b/examples/rust-string/.cargo/config @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" \ No newline at end of file diff --git a/examples/rust-string/Cargo.toml b/examples/rust-string/Cargo.toml new file mode 100644 index 0000000..94e91c2 --- /dev/null +++ b/examples/rust-string/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "rust_string" +version = "0.1.0" + +[lib] +crate-type = ["cdylib"] \ No newline at end of file diff --git a/examples/rust-string/README.md b/examples/rust-string/README.md new file mode 100644 index 0000000..fc2649e --- /dev/null +++ b/examples/rust-string/README.md @@ -0,0 +1,20 @@ +### Example: Rust String + +This shows an example of using Rust strings on the JVM. This builds on [rust-simple](../rust-simple). + +In this version, we do allocation and deallocation on the Rust side and we have a Java pointer object that deallocates +on finalization. Inside of Rust, we make sure not to take ownership of any of the data. To demonstrate string use, we +implement two functions on the Rust side: one for string length and another for prepending "From Rust: ". Both don't +take strings directly, but instead pointers and lengths to the byte arrays. + +To run it yourself, run the following from the root `asmble` dir: + + gradlew --no-daemon :examples:rust-string:run + +In release mode, the generated class is 128KB w/ a bit over 200 methods, but it is quite fast. The output: + + Char count of 'tester': 6 + Char count of Russian hello (Здравствуйте): 12 + From Rust: Hello, World! + +For me on Windows, the Russian word above just appears as `????????????`, but the rest is right. \ No newline at end of file diff --git a/examples/rust-string/src/lib.rs b/examples/rust-string/src/lib.rs new file mode 100644 index 0000000..3fb74de --- /dev/null +++ b/examples/rust-string/src/lib.rs @@ -0,0 +1,46 @@ +#![feature(allocator_api)] + +use std::heap::{Alloc, Heap, Layout}; +use std::ffi::{CString}; +use std::mem; +use std::os::raw::c_char; +use std::str; + +#[no_mangle] +pub extern "C" fn string_len(ptr: *mut u8, len: usize) -> usize { + unsafe { + let bytes = Vec::::from_raw_parts(ptr, len, len); + let len = str::from_utf8(&bytes).unwrap().chars().count(); + mem::forget(bytes); + len + } +} + +#[no_mangle] +pub extern "C" fn prepend_from_rust(ptr: *mut u8, len: usize) -> *const c_char { + unsafe { + let bytes = Vec::::from_raw_parts(ptr, len, len); + let s = str::from_utf8(&bytes).unwrap(); + mem::forget(s); + let cstr = CString::new(format!("From Rust: {}", s)).unwrap(); + let ret = cstr.as_ptr(); + mem::forget(cstr); + return ret + } +} + +#[no_mangle] +pub extern "C" fn alloc(size: usize) -> *mut u8 { + unsafe { + let layout = Layout::from_size_align(size, mem::align_of::()).unwrap(); + Heap.alloc(layout).unwrap() + } +} + +#[no_mangle] +pub extern "C" fn dealloc(ptr: *mut u8, size: usize) { + unsafe { + let layout = Layout::from_size_align(size, mem::align_of::()).unwrap(); + Heap.dealloc(ptr, layout); + } +} \ No newline at end of file diff --git a/examples/rust-string/src/main/java/asmble/examples/ruststring/Lib.java b/examples/rust-string/src/main/java/asmble/examples/ruststring/Lib.java new file mode 100644 index 0000000..5d19847 --- /dev/null +++ b/examples/rust-string/src/main/java/asmble/examples/ruststring/Lib.java @@ -0,0 +1,93 @@ +package asmble.examples.ruststring; + +import asmble.generated.RustString; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +// Not thread safe! +class Lib { + private final RustString rustString; + + Lib(RustString rustString) { + this.rustString = rustString; + } + + int stringLength(String str) { + Ptr strPtr = ptrFromString(str); + return rustString.string_len(strPtr.offset, strPtr.size); + } + + String prependFromRust(String str) { + Ptr strPtr = ptrFromString(str); + int nullTermOffset = rustString.prepend_from_rust(strPtr.offset, strPtr.size); + return nullTermedStringFromOffset(nullTermOffset); + } + + private Ptr ptrFromString(String str) { + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + Ptr ptr = new Ptr(bytes.length); + ptr.put(bytes); + return ptr; + } + + private String nullTermedStringFromOffset(int offset) { + ByteBuffer memory = rustString.getMemory(); + memory.position(offset); + // We're going to turn the mem into an input stream. This is the + // reasonable way to stream a UTF8 read using standard Java libs + // that I could find. + InputStreamReader r = new InputStreamReader(new InputStream() { + @Override + public int read() throws IOException { + if (!memory.hasRemaining()) { + return -1; + } + return memory.get() & 0xFF; + } + }, StandardCharsets.UTF_8); + StringBuilder builder = new StringBuilder(); + try { + while (true) { + int c = r.read(); + if (c <= 0) { + break; + } + builder.append((char) c); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + rustString.dealloc(offset, memory.position() - offset); + return builder.toString(); + } + + class Ptr { + final int offset; + final int size; + + Ptr(int offset, int size) { + this.offset = offset; + this.size = size; + } + + Ptr(int size) { + this(rustString.alloc(size), size); + } + + void put(byte[] bytes) { + // Yeah, yeah, not thread safe + ByteBuffer memory = rustString.getMemory(); + memory.position(offset); + memory.put(bytes); + } + + @Override + protected void finalize() throws Throwable { + rustString.dealloc(offset, size); + } + } +} \ No newline at end of file diff --git a/examples/rust-string/src/main/java/asmble/examples/ruststring/Main.java b/examples/rust-string/src/main/java/asmble/examples/ruststring/Main.java new file mode 100644 index 0000000..a0cac02 --- /dev/null +++ b/examples/rust-string/src/main/java/asmble/examples/ruststring/Main.java @@ -0,0 +1,17 @@ +package asmble.examples.ruststring; + +import asmble.generated.RustString; + +public class Main { + // 20 pages is good for now + private static final int PAGE_SIZE = 65536; + private static final int MAX_MEMORY = 20 * PAGE_SIZE; + + public static void main(String[] args) { + Lib lib = new Lib(new RustString(MAX_MEMORY)); + System.out.println("Char count of 'tester': " + lib.stringLength("tester")); + String russianHello = "\u0417\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435"; + System.out.println("Char count of Russian hello (" + russianHello + "): " + lib.stringLength(russianHello)); + System.out.println(lib.prependFromRust("Hello, World!")); + } +} diff --git a/settings.gradle b/settings.gradle index 163d42b..5be81bb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,4 +2,5 @@ rootProject.name = 'asmble' include 'annotations', 'compiler', 'emscripten-runtime', - 'examples:rust-simple' \ No newline at end of file + 'examples:rust-simple', + 'examples:rust-string' \ No newline at end of file