diff --git a/.travis.yml b/.travis.yml index 042bdb9..18f1ab6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,18 @@ language: java jdk: - - oraclejdk8 + - oraclejdk8 +env: + - RUST_CHANNEL=stable + - RUST_CHANNEL=nightly +matrix: + allow_failures: + - env: RUST_CHANNEL=nightly before_install: - - yes | sudo add-apt-repository ppa:hansjorg/rust - - sudo apt-get update + - "curl https://sh.rustup.rs -sSf > rustup && chmod +x rustup" + - "./rustup -y" + - "export PATH=$PATH:~/.cargo/bin" + - rustc --version install: - - sudo apt-get install rust-nightly + - rustup toolchain install ${RUST_CHANNEL} + - rustup override set ${RUST_CHANNEL} + - rustc --version diff --git a/README.md b/README.md index 51b7ccb..e6ee394 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,14 @@ An example project showing how to call into Rust code from Java. -[![Build Status](https://travis-ci.org/drrb/java-rust-example.svg?branch=master)](https://travis-ci.org/drrb/java-rust-example) +| OSX | Linux | Windows | +| --- | ----- | ------- | +| ![OSX Build Status](https://img.shields.io/badge/build-passing%20on%20my%20laptop-brightgreen.svg) | [![Linux Build Status](https://travis-ci.org/drrb/java-rust-example.svg?branch=master)](https://travis-ci.org/drrb/java-rust-example) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/4yygb3925k7p87de/branch/master?svg=true)](https://ci.appveyor.com/project/drrb/java-rust-example/branch/master) | ## Requirements -- Tested on OSX and Linux. May also work on Windows! - Java 7+ -- Rust (tested 1.0 Beta) +- Rust (tested with 1.0, nightly) ## Contents @@ -28,12 +29,12 @@ and the [Rust code](src/main/rust/com/github/drrb/javarust/lib/greetings.rs). Th implementation is heavily commented to explain it. So far, it contains examples of the following (click the links to see!): -- *[Arguments](src/test/java/com/github/drrb/javarust/GreetingsTest.java#L42)*: passing simple arguments from Java to Rust ([Java side](src/main/java/com/github/drrb/javarust/Greetings.java#L44) / [Rust side](src/main/rust/com/github/drrb/javarust/lib/greetings.rs#L73)) -- *[Return values](src/test/java/com/github/drrb/javarust/GreetingsTest.java#L47)*: returning simple values from Rust to Java ([Java side](src/main/java/com/github/drrb/javarust/Greetings.java#L49) / [Rust side](src/main/rust/com/github/drrb/javarust/lib/greetings.rs#L86)) -- *[Struct arguments](src/test/java/com/github/drrb/javarust/GreetingsTest.java#L54)*: passing structs to Rust from Java ([Java side](src/main/java/com/github/drrb/javarust/Greetings.java#L54) / [Rust side](src/main/rust/com/github/drrb/javarust/lib/greetings.rs#L97)) -- *[Returning structs (2 examples)](src/test/java/com/github/drrb/javarust/GreetingsTest.java#L63)*: returning structs from Rust by value and by reference ([Java side](src/main/java/com/github/drrb/javarust/Greetings.java#L62) / [Rust side](src/main/rust/com/github/drrb/javarust/lib/greetings.rs#L105)) -- *[Callbacks (3 examples)](src/test/java/com/github/drrb/javarust/GreetingsTest.java#L80)*: passing callbacks to Rust that get called from the Rust code ([Java side](src/main/java/com/github/drrb/javarust/Greetings.java#L84) / [Rust side](src/main/rust/com/github/drrb/javarust/lib/greetings.rs#L122)) -- *[Freeing memory](src/test/java/com/github/drrb/javarust/GreetingsTest.java#L76)*: freeing memory allocated in Rust ([Java side](src/main/java/com/github/drrb/javarust/Greetings.java#L144) / [Rust side](src/main/rust/com/github/drrb/javarust/lib/greetings.rs#L175)) +- *[Arguments](src/test/java/com/github/drrb/javarust/GreetingsTest.java#L45)*: passing simple arguments from Java to Rust ([Java side](src/main/java/com/github/drrb/javarust/Greetings.java#L44) / [Rust side](src/main/rust/com/github/drrb/javarust/lib/greetings.rs#L81)) +- *[Return values](src/test/java/com/github/drrb/javarust/GreetingsTest.java#L50)*: returning simple values from Rust to Java ([Java side](src/main/java/com/github/drrb/javarust/Greetings.java#L49) / [Rust side](src/main/rust/com/github/drrb/javarust/lib/greetings.rs#L91)) +- *[Struct arguments](src/test/java/com/github/drrb/javarust/GreetingsTest.java#L56)*: passing structs to Rust from Java ([Java side](src/main/java/com/github/drrb/javarust/Greetings.java#L54) / [Rust side](src/main/rust/com/github/drrb/javarust/lib/greetings.rs#L100)) +- *[Returning structs (2 examples)](src/test/java/com/github/drrb/javarust/GreetingsTest.java#L65)*: returning structs from Rust by value and by reference ([Java side](src/main/java/com/github/drrb/javarust/Greetings.java#L71) / [Rust side](src/main/rust/com/github/drrb/javarust/lib/greetings.rs#L109)) +- *[Callbacks (3 examples)](src/test/java/com/github/drrb/javarust/GreetingsTest.java#L80)*: passing callbacks to Rust that get called from the Rust code ([Java side](src/main/java/com/github/drrb/javarust/Greetings.java#L84) / [Rust side](src/main/rust/com/github/drrb/javarust/lib/greetings.rs#L129)) +- *[Freeing memory](src/test/java/com/github/drrb/javarust/GreetingsTest.java#L67)*: freeing memory allocated in Rust ([Java side](src/main/java/com/github/drrb/javarust/Greetings.java#L114) / [Rust side](src/main/rust/com/github/drrb/javarust/lib/greetings.rs#L171)) ## Building and Running the Tests @@ -54,12 +55,11 @@ Hello from Rust, John ## Platform Support -This project is tested on OSX and Ubuntu. It should work on OSX, and any 32 bit -or 64 bit Gnu/Linux system. +This project is tested on OSX, Ubuntu, and Windows. It should also work on any 32 bit or 64 bit Gnu/Linux system. ## Limitations -Some of the examples leak memory. Any memory that is allocated in Rust needs to be freed manually because it's not managed by JNA. Some examples pass objects back into Rust to be dropped for this reason, but we don't clean up every thing properly (strings, for example). This is almost certainly not a limitation of Rust, but a limitation of my current understanding of Rust. +Some of the examples leak memory. Any memory that is allocated in Rust needs to be freed manually because it's not managed by JNA. Some examples pass objects back into Rust to be dropped for this reason, but we don't clean up everything properly (strings, for example). This is almost certainly not a limitation of Rust, but a limitation of my current understanding of Rust. ## License diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..7f06f19 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,21 @@ +version: '{build}' +os: Windows Server 2012 +environment: + matrix: + - JAVA_HOME: C:\Program Files\Java\jdk1.7.0 + - JAVA_HOME: C:\Program Files\Java\jdk1.8.0 +install: + - ps: choco install maven + - ps: src/build/scripts/rustup.ps1 + - ps: $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + - mvn --version + - rustc --version + - cargo --version +build_script: + - mvn --batch-mode clean package -DskipTests +test_script: + - mvn --batch-mode clean package +artifacts: + - path: target/rust-libs/* +cache: + - C:\Users\appveyor\.m2 diff --git a/generate-link-table b/generate-link-table new file mode 100755 index 0000000..f45499d --- /dev/null +++ b/generate-link-table @@ -0,0 +1,89 @@ +#!/usr/bin/env ruby + +require "ostruct" + +class Concept < OpenStruct + def to_markdown + "*#{test_link}*: #{description} (#{java_link} / #{rust_link})" + end + + def test_link + link text: name, regex: test_regex, file: "src/test/java/com/github/drrb/javarust/GreetingsTest.java" + end + + def java_link + link text: "Java side", regex: java_regex, file: "src/main/java/com/github/drrb/javarust/Greetings.java" + end + + def rust_link + link text: "Rust side", regex: rust_regex, file: "src/main/rust/com/github/drrb/javarust/lib/greetings.rs" + end + + private + def link(text:, regex:, file:) + line = find_line(regex, file) + "[#{text}](#{file}#L#{line})" + end + + def find_line(regex, file) + lines = File.read(file).lines.each_with_index + line, index = lines.find {|line, index| line =~ regex} + unless line + raise "Couldn't find #{regex.inspect} in #{file.inspect}" + end + index + 1 + end +end + +class Array + def to_markdown_points + map {|e| "- #{e}" }.join("\n") + end +end + +concepts = [ + Concept.new( + name: "Arguments", + description: "passing simple arguments from Java to Rust", + test_regex: /shouldAcceptStringParameterFromJavaToRust/, + java_regex: /void printGreeting/, + rust_regex: /pub extern fn printGreeting/ + ), + Concept.new( + name: "Return values", + description: "returning simple values from Rust to Java", + test_regex: /public void shouldAcceptStringFromJavaToRustAndReturnAnotherOne/, + java_regex: /String renderGreeting\(String name\)/, + rust_regex: /pub extern fn renderGreeting/ + ), + Concept.new( + name: "Struct arguments", + description: "passing structs to Rust from Java", + test_regex: /public void shouldAcceptAStructFromJavaToRust/, + java_regex: /String greet\(Person john\)/, + rust_regex: /pub extern fn greet\(person: &Person\)/ + ), + Concept.new( + name: "Returning structs (2 examples)", + description: "returning structs from Rust by value and by reference", + test_regex: /public void shouldGetAStructFromRustByValue/, + java_regex: /Greeting.ByValue getGreetingByValue/, + rust_regex: /pub extern fn getGreetingByValue/ + ), + Concept.new( + name: "Callbacks (3 examples)", + description: "passing callbacks to Rust that get called from the Rust code", + test_regex: /public void shouldGetAStringFromRustInACallback/, + java_regex: /void callMeBack\(GreetingCallback callback\)/, + rust_regex: /pub extern fn callMeBack/ + ), + Concept.new( + name: "Freeing memory", + description: "freeing memory allocated in Rust", + test_regex: /try \(Greeting greeting/, + java_regex: /void dropGreeting\(Greeting greeting\)/, + rust_regex: /pub extern fn dropGreeting/ + ) +] + +puts concepts.map(&:to_markdown).to_markdown_points diff --git a/pom.xml b/pom.xml index 0c0c607..23db604 100644 --- a/pom.xml +++ b/pom.xml @@ -81,6 +81,12 @@ along with this program. If not, see . org.apache.maven.plugins maven-surefire-plugin 2.18.1 + + + -Djna.nosys=true + 1 + false + diff --git a/src/build/scripts/rustup.ps1 b/src/build/scripts/rustup.ps1 new file mode 100644 index 0000000..fbb9138 --- /dev/null +++ b/src/build/scripts/rustup.ps1 @@ -0,0 +1,23 @@ +$ErrorActionPreference = "Stop" + +if ([environment]::Is64BitOperatingSystem) { + $arch = "x86_64" + $install_dir = "C:\Program Files\Rust" +} else { + $arch = "i686" + $install_dir = "C:\Program Files (x86)\Rust" +} + +$package = "rust-nightly-$($arch)-pc-windows-gnu.exe" +$url = "https://static.rust-lang.org/dist/$($package)" + +echo "Downloading Rust and Cargo from $($url)" +Start-FileDownload $url + +echo "Installing Rust" +Start-Process ".\$($package)" -ArgumentList "/VERYSILENT /NORESTART" -NoNewWindow -Wait + +echo "Refreshing Path" +$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "User") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "Machine") + +echo "Rust and Cargo are ready to roll!" diff --git a/src/main/java/com/github/drrb/javarust/Greeting.java b/src/main/java/com/github/drrb/javarust/Greeting.java index c860dd1..020a80d 100644 --- a/src/main/java/com/github/drrb/javarust/Greeting.java +++ b/src/main/java/com/github/drrb/javarust/Greeting.java @@ -17,15 +17,18 @@ package com.github.drrb.javarust; import com.sun.jna.Structure; -import static java.util.Arrays.asList; + +import java.io.Closeable; import java.util.List; +import static java.util.Arrays.asList; + /** * A struct that we return from Rust to Java. * * This is the Java representation of the Greeting struct in Rust. */ -public class Greeting extends Structure { +public class Greeting extends Structure implements Closeable { public static class ByReference extends Greeting implements Structure.ByReference { } @@ -43,4 +46,15 @@ public String getText() { protected List getFieldOrder() { return asList("text"); } + + @Override + public void close() { + // Turn off "auto-synch". If it is on, JNA will automatically read all fields + // from the struct's memory and update them on the Java object. This synchronization + // occurs after every native method call. If it occurs after we drop the struct, JNA + // will try to read from the freed memory and cause a segmentation fault. + setAutoSynch(false); + // Send the struct back to rust for the memory to be freed + Greetings.INSTANCE.dropGreeting(this); + } } diff --git a/src/main/java/com/github/drrb/javarust/GreetingSet.java b/src/main/java/com/github/drrb/javarust/GreetingSet.java index a8cfda6..f793131 100644 --- a/src/main/java/com/github/drrb/javarust/GreetingSet.java +++ b/src/main/java/com/github/drrb/javarust/GreetingSet.java @@ -17,6 +17,8 @@ package com.github.drrb.javarust; import com.sun.jna.Structure; + +import java.io.Closeable; import java.util.Arrays; import java.util.List; @@ -24,7 +26,7 @@ * A struct that contains an array of structs. This is the Java representation * of the GreetingSet struct in Rust (see the Rust code). */ -public class GreetingSet extends Structure { +public class GreetingSet extends Structure implements Closeable { public static class ByReference extends GreetingSet implements Structure.ByReference { } @@ -38,9 +40,9 @@ public static class ByValue extends GreetingSet implements Structure.ByValue { * Actually, this is a pointer to a bunch of struct instances that are next * to each other in memory. We cast it to an array in {@link #getGreetings()}. * - * NB: We need to explicity specify that the field is a pointer (i.e. we need + * NB: We need to explicitly specify that the field is a pointer (i.e. we need * to use ByReference) because, by default, JNA assumes that struct fields - * are not pointers (i.e. the if you just say "Greeting", JNA assumes + * are not pointers (i.e. if you just say "Greeting", JNA assumes * "Greeting.ByValue" here). */ public Greeting.ByReference greetings; @@ -80,4 +82,20 @@ public List getGreetings() { protected List getFieldOrder() { return Arrays.asList("greetings", "numberOfGreetings"); } + + /** + * Send the GreetingSet back to Rust to be dropped. + * + * We do this because JNA doesn't free the memory when the object is garbage collected. + */ + @Override + public void close() { + // Turn off "auto-synch". If it is on, JNA will automatically read all fields + // from the struct's memory and update them on the Java object. This synchronization + // occurs after every native method call. If it occurs after we drop the struct, JNA + // will try to read from the freed memory and cause a segmentation fault. + setAutoSynch(false); + // Send the struct back to rust for the memory to be freed + Greetings.INSTANCE.dropGreetingSet(this); + } } diff --git a/src/main/rust/com/github/drrb/javarust/lib/greetings.rs b/src/main/rust/com/github/drrb/javarust/lib/greetings.rs index 879b1fc..213c7b1 100644 --- a/src/main/rust/com/github/drrb/javarust/lib/greetings.rs +++ b/src/main/rust/com/github/drrb/javarust/lib/greetings.rs @@ -21,28 +21,23 @@ use std::ffi::{CStr,CString}; use std::str; use std::mem; - -// Normally we'd get these from libc, but that's unstable in 1.0 Beta -mod mylibc { - #[allow(non_camel_case_types)] - pub type c_int = i32; - #[allow(non_camel_case_types)] - pub type c_char = i8; -} -use mylibc::c_int; -use mylibc::c_char; +use std::os::raw::c_char; // GreetingSet corresponds to com.github.drrb.javarust.GreetingSet in Java. It is marked with // repr(c), as are all the structs passed back to Java. This makes sure the structs are represented // in memory in a way JNA can read them. #[repr(C)] pub struct GreetingSet { - // A pointer to an array of Greetings. This is converted to a Greeting.ByReference by JNA. - greetings: Box<[Greeting]>, - // The size of the array. We need to pass it back to Java so that we know how long the array - // is (JNA can't guess the size). We need to do this with all arrays created in Java and read - // in Rust (or vise-versa). This c_int is converted to a Java int by JNA. - number_of_greetings: c_int + // A struct that includes a pointer to an array of Greetings and a size. JNA will convert this + // to two fields: a Greeting.ByReference and an int. + greetings: Box<[Greeting]> +} + +impl Drop for GreetingSet { + fn drop(&mut self) { + // Print a message when we drop the object, so that we know we're not leaking memory + println!("Dropping GreetingSet"); + } } // Greeting corresponds to com.github.drrb.javarust.Greeting in Java. It is marked with @@ -63,6 +58,12 @@ impl Greeting { } } +impl Drop for Greeting { + fn drop(&mut self) { + println!("Dropping Greeting: {}", to_string(self.text)); + } +} + #[repr(C)] #[allow(missing_copy_implementations)] pub struct Person { @@ -144,17 +145,14 @@ pub extern fn callMeBack(callback: extern "stdcall" fn(*const c_char)) { /// In this example we send a pointer to a struct back to Java via the callback. #[no_mangle] #[allow(non_snake_case)] -pub extern fn sendGreetings(callback: extern "C" fn(Box)) { +pub extern fn sendGreetings(callback: extern "C" fn(&GreetingSet)) { let greetings = vec![ Greeting::new("Hello!"), Greeting::new("Hello again!") ]; - let num_greetings = greetings.len(); - let set = Box::new(GreetingSet { - // Get a pointer to the vector as an array, so that we can pass it back to Java - greetings: greetings.into_boxed_slice(), - // Also return the length of the array, so that we can create the array back in Java - number_of_greetings: num_greetings as c_int - }); - callback(set); + let set = GreetingSet { + // Get a pointer to the vector as an array, so that we can pass it back to Java + greetings: greetings.into_boxed_slice() + }; + callback(&set); // Let the callback "borrow" the set. Rust will destroy it after calling the callback } /// Example of returning a more complicated struct from Rust @@ -162,11 +160,9 @@ pub extern fn sendGreetings(callback: extern "C" fn(Box)) { #[allow(non_snake_case)] pub extern fn renderGreetings() -> Box { let greetings = vec![ Greeting::new("Hello!"), Greeting::new("Hello again!") ]; - let num_greetings = greetings.len(); Box::new(GreetingSet { - greetings: greetings.into_boxed_slice(), - number_of_greetings: num_greetings as c_int + greetings: greetings.into_boxed_slice() }) } @@ -180,7 +176,7 @@ pub extern fn dropGreeting(_: Box) { #[no_mangle] #[allow(non_snake_case)] pub extern fn dropGreetingSet(_: Box) { - // Do nothing here. Because we own the GreetingSet here (we're using a Box) and we're not + // Do nothing here. Because we own the GreetingSet here and we're not // returning it, Rust will assume we don't want it anymore and clean it up. } @@ -196,6 +192,6 @@ fn to_ptr(string: String) -> *const c_char { let ptr = cs.as_ptr(); // Tell Rust not to clean up the string while we still have a pointer to it. // Otherwise, we'll get a segfault. - unsafe { mem::forget(cs) }; + mem::forget(cs); ptr } diff --git a/src/test/java/com/github/drrb/javarust/GreetingsTest.java b/src/test/java/com/github/drrb/javarust/GreetingsTest.java index 469331a..0cb09e8 100644 --- a/src/test/java/com/github/drrb/javarust/GreetingsTest.java +++ b/src/test/java/com/github/drrb/javarust/GreetingsTest.java @@ -18,16 +18,18 @@ import com.github.drrb.javarust.Greetings.GreetingCallback; import com.github.drrb.javarust.Greetings.GreetingSetCallback; -import static com.github.drrb.javarust.test.Matchers.*; import com.github.drrb.javarust.test.MethodPrintingRule; -import static java.util.Arrays.asList; -import java.util.LinkedList; -import java.util.List; -import static org.junit.Assert.assertThat; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import java.util.LinkedList; +import java.util.List; + +import static com.github.drrb.javarust.test.Matchers.is; +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertThat; + public class GreetingsTest { @Rule @@ -61,19 +63,17 @@ public void shouldAcceptAStructFromJavaToRust() { @Test public void shouldGetAStructFromRustByValue() { - Greeting greeting = library.getGreetingByValue(); - assertThat(greeting.text, is("Hello from Rust!")); + // Using try-with-resources so that memory gets cleaned up. See Greeting.close() + try (Greeting greeting = library.getGreetingByValue()) { + assertThat(greeting.text, is("Hello from Rust!")); + } } @Test public void shouldGetAStructFromRustByReference() { - Greeting greeting = library.getGreetingByReference(); - - assertThat(greeting.text, is("Hello from Rust!")); - - // Free the memory after using it. We need to do this because JNA assumes - // that the memory is owned by Rust, so Rust must clean it up. - library.dropGreeting(greeting); + try (Greeting greeting = library.getGreetingByReference()) { + assertThat(greeting.text, is("Hello from Rust!")); + } } @Test @@ -84,7 +84,7 @@ public void apply(String greeting) { greetings.add(greeting); } }); - assertThat(greetings, is(asList("Hello there!"))); + assertThat(greetings, contains("Hello there!")); } @Test @@ -97,25 +97,22 @@ public void apply(GreetingSet.ByReference greetingSet) { }); List greetingStrings = new LinkedList<>(); - for (Greeting greeting: greetings) { + for (Greeting greeting : greetings) { greetingStrings.add(greeting.getText()); } - assertThat(greetingStrings, is(asList("Hello!", "Hello again!"))); - for (Greeting greeting: greetings) { - library.dropGreeting(greeting); - } + assertThat(greetingStrings, contains("Hello!", "Hello again!")); } @Test public void shouldGetAStructFromRustContainingAnArrayOfStructs() { - GreetingSet result = library.renderGreetings(); - List greetings = new LinkedList<>(); - for (Greeting greeting: result.getGreetings()) { - greetings.add(greeting.getText()); - } + try (GreetingSet result = library.renderGreetings()) { + List greetings = new LinkedList<>(); + for (Greeting greeting : result.getGreetings()) { + greetings.add(greeting.getText()); + } - assertThat(greetings, is(asList("Hello!", "Hello again!"))); - library.dropGreetingSet(result); + assertThat(greetings, contains("Hello!", "Hello again!")); + } } }