diff --git a/.travis.yml b/.travis.yml index 2cd5d1a..18f1ab6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,18 @@ language: java jdk: - - oraclejdk8 + - oraclejdk8 env: - - RUST_CHANNEL=stable - - RUST_CHANNEL=nightly + - 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-${RUST_CHANNEL} + - rustup toolchain install ${RUST_CHANNEL} + - rustup override set ${RUST_CHANNEL} + - rustc --version diff --git a/README.md b/README.md index 25d5a1a..e6ee394 100644 --- a/README.md +++ b/README.md @@ -29,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 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/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 568dae6..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. } 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!")); + } } }