diff --git a/.gitignore b/.gitignore index 0718b94..7ba293c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ /examples/c-simple/build /examples/go-simple/bin /examples/go-simple/build +/examples/go-util/bin +/examples/go-util/build /examples/rust-simple/Cargo.lock /examples/rust-simple/bin /examples/rust-simple/build diff --git a/build.gradle b/build.gradle index 330742e..def46de 100644 --- a/build.gradle +++ b/build.gradle @@ -120,7 +120,7 @@ project(':examples') { // args 'help', 'compile' def outFile = 'build/wasm-classes/' + wasmCompiledClassName.replace('.', '/') + '.class' file(outFile).parentFile.mkdirs() - args 'compile', 'build/lib.wasm', wasmCompiledClassName, '-out', outFile, '-log', 'debug' + args 'compile', 'build/lib.wasm', wasmCompiledClassName, '-out', outFile } } @@ -189,6 +189,7 @@ project(':examples:go-simple') { apply plugin: 'application' ext.wasmCompiledClassName = 'asmble.generated.GoSimple' dependencies { + compile project(':examples:go-util') compile files('build/wasm-classes') } compileJava { @@ -197,6 +198,9 @@ project(':examples:go-simple') { mainClassName = 'asmble.examples.gosimple.Main' } +project(':examples:go-util') { +} + project(':examples:rust-regex') { apply plugin: 'application' apply plugin: 'me.champeau.gradle.jmh' diff --git a/examples/README.md b/examples/README.md index 02b55e9..0e627ac 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,6 +2,12 @@ Below are some examples. +### Go + +Compile Go to WASM and then to the JVM. In order of complexity: + +* [go-simple](go-simple) + ### Rust Compile Rust to WASM and then to the JVM. In order of complexity: diff --git a/examples/go-simple/README.md b/examples/go-simple/README.md new file mode 100644 index 0000000..81d04ea --- /dev/null +++ b/examples/go-simple/README.md @@ -0,0 +1,11 @@ +### Example: Go Simple + +This runs a simple "Hello, World" in Go. To execute, from the base of the repo run: + + path/to/gradle :examples:go-simple:run + +This will create a WASM file in `build/lib.wasm`. Then it will compile it to a JVM class named +`asmble.generated.GoSimple`. Finally it will execute `asmble.examples.gosimple.Main::main` which, with some help from +[go-util](../go-util), will run it. Output is: + + Hello, World \ No newline at end of file diff --git a/examples/go-simple/simple.go b/examples/go-simple/simple.go index 2ca0d5e..16e2b22 100644 --- a/examples/go-simple/simple.go +++ b/examples/go-simple/simple.go @@ -1,9 +1,5 @@ package main -import ( - "fmt" -) - func main() { - fmt.Println("Hello, World!") + println("Hello, World!") } diff --git a/examples/go-simple/src/main/java/asmble/examples/gosimple/Main.java b/examples/go-simple/src/main/java/asmble/examples/gosimple/Main.java new file mode 100644 index 0000000..674072f --- /dev/null +++ b/examples/go-simple/src/main/java/asmble/examples/gosimple/Main.java @@ -0,0 +1,12 @@ +package asmble.examples.gosimple; + +import asmble.examples.goutil.Executor; +import asmble.generated.GoSimple; + +public class Main { + public static void main(String[] args) { + Integer exitCode = new Executor<>(GoSimple::new).run(GoSimple::run, "test-app"); + if (exitCode == null) throw new IllegalStateException("Did not get exit code"); + if (exitCode != 0) throw new IllegalStateException("Expected exit code 0, got: " + exitCode); + } +} diff --git a/examples/go-util/README.md b/examples/go-util/README.md new file mode 100644 index 0000000..82709e1 --- /dev/null +++ b/examples/go-util/README.md @@ -0,0 +1,4 @@ +### goutil + +This is helper code for other examples. Specifically it contains `asmble.examples.goutil.Executor` which helps bootstrap +and provide runtime for Go code. \ No newline at end of file diff --git a/examples/go-util/src/main/java/asmble/examples/goutil/Executor.java b/examples/go-util/src/main/java/asmble/examples/goutil/Executor.java new file mode 100644 index 0000000..993e430 --- /dev/null +++ b/examples/go-util/src/main/java/asmble/examples/goutil/Executor.java @@ -0,0 +1,166 @@ +package asmble.examples.goutil; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.time.Instant; +import java.util.*; +import java.util.function.IntConsumer; + +public class Executor { + @FunctionalInterface + public interface ConstructorNoJs { + T create(ByteBuffer mem, MethodHandle debug, MethodHandle runtimeWasmExit, MethodHandle runtimeWasmWrite, + MethodHandle runtimeNanotime, MethodHandle runtimeWalltime, MethodHandle runtimeScheduleCallback, + MethodHandle runtimeClearScheduledCallback, MethodHandle runtimeGetRandomData); + } + + @FunctionalInterface + public interface Run { + void run(T instance, int argc, int argv); + } + + protected static final int PAGE_SIZE = 65536; + + public Random random = new SecureRandom(); + public OutputStream out = System.out; + + // Always little endian + protected final ByteBuffer mem; + protected final T instance; + protected IntConsumer debug; + protected Integer exitCode; + + public Executor(ConstructorNoJs constructor) { + this(16384, constructor); + } + + public Executor(int maxMemPages, ConstructorNoJs constructor) { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + MethodType singleIntParam = MethodType.methodType(Void.TYPE, Integer.TYPE); + try { + // Set to little endian by the constructor, not us + mem = ByteBuffer.allocateDirect(maxMemPages * PAGE_SIZE); + instance = constructor.create( + mem, + lookup.bind(this, "debug", singleIntParam), + lookup.bind(this, "runtimeWasmExit", singleIntParam), + lookup.bind(this, "runtimeWasmWrite", singleIntParam), + lookup.bind(this, "runtimeNanotime", singleIntParam), + lookup.bind(this, "runtimeWalltime", singleIntParam), + lookup.bind(this, "runtimeScheduleCallback", singleIntParam), + lookup.bind(this, "runtimeClearScheduledCallback", singleIntParam), + lookup.bind(this, "runtimeGetRandomData", singleIntParam) + ); + } catch (NoSuchMethodException|IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public Integer run(Run run, String... args) { + return run(run, args, Collections.emptyMap()); + } + + public Integer run(Run run, String[] args, Map env) { + // Argc + argv, then env appended to argv as key=val + int offset = 4096; + int argc = args.length; + List strPtrs = new ArrayList<>(); + for (String arg : args) { + strPtrs.add(offset); + offset += newString(arg, offset); + } + for (Map.Entry var : env.entrySet()) { + strPtrs.add(offset); + offset += newString(var.getKey() + "=" + var.getValue(), offset); + } + int argv = offset; + for (int strPtr : strPtrs) { + mem.putLong(offset, strPtr); + offset += 8; + } + // Run and return exit code + run.run(instance, argc, argv); + return exitCode; + } + + // Returns size, aligned to 8 + protected int newString(String str, int ptr) { + byte[] bytes = (str + '\0').getBytes(StandardCharsets.UTF_8); + putBytes(ptr, bytes); + return bytes.length + (8 - (bytes.length % 8)); + } + + protected byte[] getBytes(int offset, byte[] bytes) { + ByteBuffer buf = mem.duplicate(); + buf.position(offset); + buf.get(bytes); + return bytes; + } + + protected void putBytes(int offset, byte[] bytes) { + ByteBuffer buf = mem.duplicate(); + buf.position(offset); + buf.put(bytes); + } + + protected void debug(int v) { + if (debug != null) debug.accept(v); + } + + protected void runtimeWasmExit(int sp) { + exitCode = mem.getInt(sp + 8); + } + + protected void wasmExit(int exitCode) { + this.exitCode = exitCode; + } + + protected void runtimeWasmWrite(int sp) { + long fd = mem.getLong(sp + 8); + long ptr = mem.getLong(sp + 16); + int len = mem.getInt(sp + 24); + wasmWrite(fd, getBytes((int) ptr, new byte[len])); + } + + protected void wasmWrite(long fd, byte[] bytes) { + if (fd != 2) throw new UnsupportedOperationException("Only fd 2 support on write, got " + fd); + if (out != null) { + try { + out.write(bytes); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + protected void runtimeNanotime(int sp) { + mem.putLong(sp + 8, System.nanoTime()); + } + + protected void runtimeWalltime(int sp) { + Instant now = Instant.now(); + mem.putLong(sp + 8, now.getEpochSecond()); + mem.putInt(sp + 16, now.getNano()); + } + + protected void runtimeScheduleCallback(int sp) { + throw new UnsupportedOperationException("runtime.scheduleCallback"); + } + + protected void runtimeClearScheduledCallback(int sp) { + throw new UnsupportedOperationException("runtime.clearScheduledCallback"); + } + + protected void runtimeGetRandomData(int sp) { + long len = mem.getLong(sp + 16); + byte[] bytes = new byte[(int) len]; + random.nextBytes(bytes); + putBytes((int) mem.getLong(sp + 8), bytes); + } +} diff --git a/settings.gradle b/settings.gradle index 3b6175d..77765ad 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,7 @@ include 'annotations', 'compiler', 'examples:c-simple', 'examples:go-simple', + 'examples:go-util', 'examples:rust-regex', 'examples:rust-simple', 'examples:rust-string' \ No newline at end of file