31 Commits

Author SHA1 Message Date
vms
23665fa16b initial commit 2019-09-05 17:45:42 +03:00
vms
4c65740d03 initial commit (#13) 2019-08-10 14:52:25 +03:00
vms
728b78d713 add EIC metering (#12) 2019-08-06 17:25:48 +03:00
vms
cb907ae2da Env module for gas metering (#11) 2019-08-06 10:45:16 +03:00
1d6002624f Fix memory builder (#10)
* fix memory instance creation

* change version
2019-06-03 14:02:15 +03:00
ad2b7c071f Bytebuffer abstraction (#9) 2019-05-13 16:40:07 +03:00
vms
119ce58c9e add using optional module name to registred names (#8) 2019-03-29 19:01:49 +03:00
vms
1323e02c95 fix bintray user and key properties absence (#4) 2018-11-19 15:25:31 +03:00
9172fba948 Add posibillity to add a Logger Wasm module (#3)
* Add logger module

* Add LoggerModuleTest
2018-11-15 12:38:50 +04:00
b9b45cf997 Merge pull request #2 from fluencelabs/logger
Fix logger and return C example
2018-11-09 13:47:41 +04:00
2bfa39a3c6 Tweaking after merge 2018-11-09 10:30:38 +04:00
317b608048 Merge fix for late init for logger 2018-11-09 10:28:47 +04:00
21b023f1c6 Return C example and skip it by default 2018-11-09 10:18:49 +04:00
765d8b4dba Possibility to skip examples 2018-11-09 10:03:44 +04:00
58cf836b76 Disable Go and Rust examples and up project version 2018-10-01 13:40:38 +04:00
56c2c8d672 Fix lateinit error with logger for wasm files 2018-10-01 13:39:08 +04:00
fb0be9d31a Merge remote-tracking branch 'upstream/master'
# Conflicts:
#	compiler/src/test/resources/spec
2018-09-18 15:56:16 +04:00
da70c9fca4 Fix for "lateinit property logger has not been initialized" 2018-08-28 16:50:37 +04:00
a1a5563367 Publishing the fork to binTray 2018-08-14 17:32:10 +04:00
e489e7c889 Publishing the fork to binTray 2018-08-14 17:22:30 +04:00
c1391b2701 Expected but isn't working version 2018-08-14 12:11:48 +04:00
6b28c5a93b merge original-master to master 2018-08-10 13:10:28 +04:00
1990f46743 merge asmble-master to master 2018-07-26 10:49:28 +04:00
559df45f09 Merge pull request #1 from cretz/master
Fetch master changes
2018-07-26 10:34:42 +04:00
80a8a1fbb9 Temporary disable rust-regex example 2018-07-25 10:35:37 +04:00
dd72c7124c Fix Rust Simple and Rust String examples 2018-07-25 10:21:38 +04:00
c04a3c4a9b Add some java docs 2018-07-24 11:00:41 +04:00
51520ac07d Add some java docs 2018-07-23 12:52:30 +04:00
97660de6ba Add some java docs 2018-07-20 12:52:21 +04:00
cee7a86773 Fix rust-regex example 2018-07-19 17:02:25 +04:00
cfa4a35af1 remove C example 2018-07-19 09:14:01 +04:00
39 changed files with 939 additions and 274 deletions

View File

@ -14,13 +14,18 @@ buildscript {
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'me.champeau.gradle:jmh-gradle-plugin:0.4.5'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4'
}
}
allprojects {
apply plugin: 'java'
group 'com.github.cretz.asmble'
version '0.4.0-SNAPSHOT'
version '0.4.11-fl'
// skips building and running for the specified examples
ext.skipExamples = ['c-simple', 'go-simple', 'rust-regex']
// todo disabling Rust regex is temporary because some strings in wasm code exceed the size in 65353 bytes.
repositories {
mavenCentral()
@ -34,7 +39,7 @@ project(':annotations') {
options.addStringOption 'Xdoclint:all', '-Xdoclint:-missing'
}
publishSettings(project, 'asmble-annotations', 'Asmble WASM Annotations', true)
publishSettings(project, 'asmble-annotations', 'Asmble WASM Annotations')
}
project(':compiler') {
@ -58,7 +63,7 @@ project(':compiler') {
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
}
publishSettings(project, 'asmble-compiler', 'Asmble WASM Compiler', false)
publishSettings(project, 'asmble-compiler', 'Asmble WASM Compiler')
}
project(':examples') {
@ -66,7 +71,7 @@ project(':examples') {
dependencies {
compileOnly project(':compiler')
}
// C/C++ example helpers
task cToWasm {
@ -76,7 +81,7 @@ project(':examples') {
def cFileName = fileTree(dir: 'src', includes: ['*.c']).files.iterator().next()
commandLine 'clang', '--target=wasm32-unknown-unknown-wasm', '-O3', cFileName, '-c', '-o', 'build/lib.wasm'
}
}
}
}
task showCWast(type: JavaExec) {
@ -174,11 +179,19 @@ project(':examples') {
}
project(':examples:c-simple') {
if (project.name in skipExamples) {
println("[Note!] Building and runnig for ${project.name} was skipped")
test.onlyIf { false } // explicit skipping tests
compileJava.onlyIf { false } // explicit skipping compile
return
}
apply plugin: 'application'
ext.wasmCompiledClassName = 'asmble.generated.CSimple'
dependencies {
compile files('build/wasm-classes')
}
compileJava {
dependsOn compileCWasm
}
@ -186,6 +199,12 @@ project(':examples:c-simple') {
}
project(':examples:go-simple') {
if (project.name in skipExamples) {
println("[Note!] Building and runnig for ${project.name} was skipped")
test.onlyIf { false } // explicit skipping tests
compileJava.onlyIf { false } // explicit skipping compile
return
}
apply plugin: 'application'
ext.wasmCompiledClassName = 'asmble.generated.GoSimple'
dependencies {
@ -197,30 +216,44 @@ project(':examples:go-simple') {
mainClassName = 'asmble.examples.gosimple.Main'
}
project(':examples:rust-regex') {
apply plugin: 'application'
apply plugin: 'me.champeau.gradle.jmh'
ext.wasmCompiledClassName = 'asmble.generated.RustRegex'
dependencies {
compile files('build/wasm-classes')
testCompile 'junit:junit:4.12'
}
compileJava {
dependsOn compileRustWasm
}
mainClassName = 'asmble.examples.rustregex.Main'
test {
testLogging.showStandardStreams = true
testLogging.events 'PASSED', 'SKIPPED'
}
jmh {
iterations = 5
warmupIterations = 5
fork = 3
}
}
project(':examples:rust-regex') {
if (project.name in skipExamples) {
println("[Note!] Building and runnig for ${project.name} was skipped")
test.onlyIf { false } // explicit skipping tests
compileJava.onlyIf { false } // explicit skipping compile
compileTestJava.onlyIf { false } // explicit skipping compile
return
}
apply plugin: 'application'
apply plugin: 'me.champeau.gradle.jmh'
ext.wasmCompiledClassName = 'asmble.generated.RustRegex'
dependencies {
compile files('build/wasm-classes')
testCompile 'junit:junit:4.12'
}
compileJava {
dependsOn compileRustWasm
}
mainClassName = 'asmble.examples.rustregex.Main'
test {
testLogging.showStandardStreams = true
testLogging.events 'PASSED', 'SKIPPED'
}
jmh {
iterations = 5
warmupIterations = 5
fork = 3
}
}
project(':examples:rust-simple') {
if (project.name in skipExamples) {
println("[Note!] Building and runnig for ${project.name} was skipped")
test.onlyIf { false } // explicit skipping tests
compileJava.onlyIf { false } // explicit skipping compile
return
}
apply plugin: 'application'
ext.wasmCompiledClassName = 'asmble.generated.RustSimple'
dependencies {
@ -233,6 +266,12 @@ project(':examples:rust-simple') {
}
project(':examples:rust-string') {
if (project.name in skipExamples) {
println("[Note!] Building and runnig for ${project.name} was skipped")
test.onlyIf { false } // explicit skipping tests
compileJava.onlyIf { false } // explicit skipping compile
return
}
apply plugin: 'application'
ext.wasmCompiledClassName = 'asmble.generated.RustString'
dependencies {
@ -244,75 +283,59 @@ project(':examples:rust-string') {
mainClassName = 'asmble.examples.ruststring.Main'
}
def publishSettings(project, projectName, projectDescription, includeJavadoc) {
def publishSettings(project, projectName, projectDescription) {
project.with {
if (!project.hasProperty('ossrhUsername')) return
apply plugin: 'com.jfrog.bintray'
apply plugin: 'maven-publish'
apply plugin: 'maven'
apply plugin: 'signing'
archivesBaseName = projectName
task packageSources(type: Jar) {
task sourcesJar(type: Jar) {
from sourceSets.main.allJava
classifier = 'sources'
from sourceSets.main.allSource
}
if (includeJavadoc) {
task packageJavadoc(type: Jar, dependsOn: 'javadoc') {
from javadoc.destinationDir
classifier = 'javadoc'
}
} else {
task packageJavadoc(type: Jar) {
// Empty to satisfy Sonatype's javadoc.jar requirement
classifier 'javadoc'
}
}
artifacts {
archives packageSources, packageJavadoc
}
signing {
sign configurations.archives
}
uploadArchives {
repositories {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/') {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots/') {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
pom.project {
name projectName
packaging 'jar'
description projectDescription
url 'https://github.com/cretz/asmble'
scm {
connection 'scm:git:git@github.com:cretz/asmble.git'
developerConnection 'scm:git:git@github.com:cretz/asmble.git'
url 'git@github.com:cretz/asmble.git'
}
licenses {
license {
name 'MIT License'
url 'https://opensource.org/licenses/MIT'
}
}
developers {
developer {
id 'cretz'
name 'Chad Retz'
url 'https://github.com/cretz'
}
}
}
publishing {
publications {
MyPublication(MavenPublication) {
from components.java
groupId group
artifactId projectName
artifact sourcesJar
version version
}
}
}
bintray {
if(!hasProperty("bintrayUser") || !hasProperty("bintrayKey")) {
return
}
user = bintrayUser
key = bintrayKey
publications = ['MyPublication']
//[Default: false] Whether to override version artifacts already published
override = false
//[Default: false] Whether version should be auto published after an upload
publish = true
pkg {
repo = 'releases'
name = projectName
userOrg = 'fluencelabs'
licenses = ['MIT']
vcsUrl = 'https://github.com/fluencelabs/asmble'
version {
name = project.version
desc = projectDescription
released = new Date()
vcsTag = project.version
}
}
}
}
}
}

View File

@ -0,0 +1,40 @@
package asmble.compile.jvm;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* The abstraction that describes work with the memory of the virtual machine.
*/
public abstract class MemoryBuffer {
/**
* The default implementation of MemoryBuffer that based on java.nio.DirectByteBuffer
*/
public static MemoryBuffer init(int capacity) {
return new MemoryByteBuffer(ByteBuffer.allocateDirect(capacity));
}
public abstract int capacity();
public abstract int limit();
public abstract MemoryBuffer clear();
public abstract MemoryBuffer limit(int newLimit);
public abstract MemoryBuffer position(int newPosition);
public abstract MemoryBuffer order(ByteOrder order);
public abstract MemoryBuffer duplicate();
public abstract MemoryBuffer put(byte[] arr, int offset, int length);
public abstract MemoryBuffer put(byte[] arr);
public abstract MemoryBuffer put(int index, byte b);
public abstract MemoryBuffer putInt(int index, int n);
public abstract MemoryBuffer putLong(int index, long n);
public abstract MemoryBuffer putDouble(int index, double n);
public abstract MemoryBuffer putShort(int index, short n);
public abstract MemoryBuffer putFloat(int index, float n);
public abstract byte get(int index);
public abstract int getInt(int index);
public abstract long getLong(int index);
public abstract short getShort(int index);
public abstract float getFloat(int index);
public abstract double getDouble(int index);
public abstract MemoryBuffer get(byte[] arr);
}

View File

@ -0,0 +1,8 @@
package asmble.compile.jvm;
/**
* Interface to initialize MemoryBuffer
*/
public interface MemoryBufferBuilder {
MemoryBuffer build(int capacity);
}

View File

@ -81,9 +81,10 @@ public class SplitMethod {
localsMap.put(index, args.size() - 1);
});
// Create the new method
String name = orig.name.replace("<", "__").replace(">", "__") + "$split";
MethodNode newMethod = new MethodNode(api,
Opcodes.ACC_STATIC + Opcodes.ACC_PRIVATE + Opcodes.ACC_SYNTHETIC, orig.name + "$split",
Type.getMethodDescriptor(Type.getType(Object[].class), args.toArray(new Type[0])), null, null);
Opcodes.ACC_STATIC + Opcodes.ACC_PRIVATE + Opcodes.ACC_SYNTHETIC, name,
Type.getMethodDescriptor(Type.getType(Object[].class), args.toArray(new Type[0])), null, null);
// Add the written locals to the map that are not already there
int newLocalIndex = args.size();
for (Integer key : splitPoint.localsWritten.keySet()) {
@ -102,9 +103,13 @@ public class SplitMethod {
for (int i = 0; i < splitPoint.length; i++) {
AbstractInsnNode insn = orig.instructions.get(i + splitPoint.start);
// Skip frames
if (insn instanceof FrameNode) continue;
if (insn instanceof FrameNode) {
insn.accept(newMethod);
continue;
}
// Store the label
if (insn instanceof LabelNode) seenLabels.add(((LabelNode) insn).getLabel());
if (insn instanceof LabelNode)
seenLabels.add(((LabelNode) insn).getLabel());
// Change the local if needed
if (insn instanceof VarInsnNode) {
insn = insn.clone(Collections.emptyMap());
@ -168,13 +173,13 @@ public class SplitMethod {
}
protected MethodNode createTrimmedMethod(String owner, MethodNode orig,
MethodNode splitOff, Splitter.SplitPoint splitPoint) {
MethodNode splitOff, Splitter.SplitPoint splitPoint) {
// The trimmed method is the same as the original, yet the split area is replaced with a call to the split off
// portion. Before calling the split-off, we have to add locals to the stack part. Then afterwards, we have to
// replace the stack and written locals.
// Effectively clone the orig
MethodNode newMethod = new MethodNode(api, orig.access, orig.name, orig.desc,
orig.signature, orig.exceptions.toArray(new String[0]));
orig.signature, orig.exceptions.toArray(new String[0]));
orig.accept(newMethod);
// Remove all insns, we'll re-add the ones outside the split range
newMethod.instructions.clear();
@ -183,11 +188,18 @@ public class SplitMethod {
Set<Label> seenLabels = new HashSet<>();
// Also keep track of the locals that have been stored, need to know
Set<Integer> seenStoredLocals = new HashSet<>();
int paramOffset = 0;
// If this is an instance method, we consider "0" (i.e. "this") as seen
if ((orig.access & Opcodes.ACC_STATIC) == 0) seenStoredLocals.add(0);
if ((orig.access & Opcodes.ACC_STATIC) == 0) {
seenStoredLocals.add(0);
paramOffset = 1;
}
// We also consider parameters as seen
int paramCount = Type.getArgumentTypes(orig.desc).length;
for (int i = 0; i < paramCount; i++) seenStoredLocals.add(i + paramOffset);
// Add the insns before split
for (int i = 0; i < splitPoint.start; i++) {
AbstractInsnNode insn = orig.instructions.get(i + splitPoint.start);
AbstractInsnNode insn = orig.instructions.get(i);
// Skip frames
if (insn instanceof FrameNode) continue;
// Record label
@ -255,7 +267,7 @@ public class SplitMethod {
}
// Now we have restored all locals and all stack...add the rest of the insns after the split
for (int i = splitPoint.start + splitPoint.length; i < orig.instructions.size(); i++) {
AbstractInsnNode insn = orig.instructions.get(i + splitPoint.start);
AbstractInsnNode insn = orig.instructions.get(i);
// Skip frames
if (insn instanceof FrameNode) continue;
// Record label

View File

@ -67,7 +67,7 @@ public class Splitter implements Iterable<Splitter.SplitPoint> {
public final int length;
public SplitPoint(SortedMap<Integer, Type> localsRead, SortedMap<Integer, Type>localsWritten,
List<Type> neededFromStackAtStart, List<Type> putOnStackAtEnd, int start, int length) {
List<Type> neededFromStackAtStart, List<Type> putOnStackAtEnd, int start, int length) {
this.localsRead = localsRead;
this.localsWritten = localsWritten;
this.neededFromStackAtStart = neededFromStackAtStart;
@ -123,11 +123,22 @@ public class Splitter implements Iterable<Splitter.SplitPoint> {
return ret;
}
private int shiftIndexValue = 1;
protected SplitPoint nextOrNull() {
// Try for each index
while (++currIndex + minSize <= insns.length) {
if(shiftIndexValue > 0) {
currIndex += shiftIndexValue;
} else {
++currIndex;
}
SplitPoint longest = longestForCurrIndex();
if (longest != null) return longest;
if (longest != null) {
shiftIndexValue += longest.length;
return longest;
}
}
return null;
}
@ -139,6 +150,8 @@ public class Splitter implements Iterable<Splitter.SplitPoint> {
InsnTraverseInfo info = new InsnTraverseInfo();
info.startIndex = currIndex;
info.endIndex = Math.min(currIndex + maxSize - 1, insns.length - 1);
// Reduce the end by special calls
constrainEndByInvokeSpecial(info);
// Reduce the end based on try/catch blocks the start is in or that jump to
constrainEndByTryCatchBlocks(info);
// Reduce the end based on any jumps within
@ -146,12 +159,23 @@ public class Splitter implements Iterable<Splitter.SplitPoint> {
// Reduce the end based on any jumps into
constrainEndByExternalJumps(info);
// Make sure we didn't reduce the end too far
if (info.getSize() < minSize) return null;
//if (info.getSize() < minSize) return null;
// Now that we have our largest range from the start index, we can go over each updating the local refs and stack
// For the stack, we are going to use the
return splitPointFromInfo(info);
}
protected void constrainEndByInvokeSpecial(InsnTraverseInfo info) {
// Can't have an invoke special of <init>
for (int i = info.startIndex; i <= info.endIndex; i++) {
AbstractInsnNode node = insns[i];
if (node.getOpcode() == Opcodes.INVOKESPECIAL && ((MethodInsnNode) node).name.equals("<init>")) {
info.endIndex = Math.max(info.startIndex, i - 1);
return;
}
}
}
protected void constrainEndByTryCatchBlocks(InsnTraverseInfo info) {
// Go over all the try/catch blocks, sorted by earliest
for (TryCatchBlockNode block : tryCatchBlocks) {
@ -251,12 +275,53 @@ public class Splitter implements Iterable<Splitter.SplitPoint> {
}
}
// visits instructions with given adaptor
private void visitInstructions(int begin, int end, AnalyzerAdapter adapter) {
if(end - begin <= 0) {
return;
}
List<Object> stack = new ArrayList<>();
List<Object> locals = new ArrayList<>();
boolean gotoSeen = false;
for (int i = begin; i < end; ++i) {
if(insns[i].getOpcode() == Opcodes.GOTO || insns[i].getOpcode() == Opcodes.ATHROW ||
(insns[i].getOpcode() >= Opcodes.TABLESWITCH && insns[i].getOpcode() <= Opcodes.RETURN) ) {
stack = adapter.stack;
locals = adapter.locals;
gotoSeen = true;
} else if(gotoSeen) {
gotoSeen = false;
if(insns[i].getOpcode() >= Opcodes.IRETURN && insns[i].getOpcode() <= Opcodes.RETURN) {
stack.clear();
locals.clear();
}
adapter.visitFrame(Opcodes.F_NEW, locals.size(), locals.toArray(), stack.size(), stack.toArray());
}
insns[i].accept(adapter);
}
AbstractInsnNode lastInsn = insns[end - 1];
if(lastInsn.getOpcode() == Opcodes.GOTO || lastInsn.getOpcode() == Opcodes.ATHROW ||
(lastInsn.getOpcode() >= Opcodes.TABLESWITCH && lastInsn.getOpcode() <= Opcodes.RETURN) ) {
if(lastInsn.getOpcode() >= Opcodes.IRETURN && lastInsn.getOpcode() <= Opcodes.RETURN) {
stack.clear();
locals.clear();
}
adapter.visitFrame(Opcodes.F_NEW, locals.size(), locals.toArray(), stack.size(), stack.toArray());
}
}
protected SplitPoint splitPointFromInfo(InsnTraverseInfo info) {
// We're going to use the analyzer adapter and run it for the up until the end, a step at a time
StackAndLocalTrackingAdapter adapter = new StackAndLocalTrackingAdapter(Splitter.this);
// Visit all of the insns up our start.
// XXX: I checked the source of AnalyzerAdapter to confirm I don't need any of the surrounding stuff
for (int i = 0; i < info.startIndex; i++) insns[i].accept(adapter);
visitInstructions(0, info.startIndex, adapter);
// Take the stack at the start and copy it off
List<Object> stackAtStart = new ArrayList<>(adapter.stack);
// Reset some adapter state
@ -264,27 +329,34 @@ public class Splitter implements Iterable<Splitter.SplitPoint> {
adapter.localsRead.clear();
adapter.localsWritten.clear();
// Now go over the remaining range
for (int i = info.startIndex; i <= info.endIndex; i++) insns[i].accept(adapter);
visitInstructions(info.startIndex, info.endIndex + 1, adapter);
// Build the split point
return new SplitPoint(
localMapFromAdapterLocalMap(adapter.localsRead, adapter.uninitializedTypes),
localMapFromAdapterLocalMap(adapter.localsWritten, adapter.uninitializedTypes),
typesFromAdapterStackRange(stackAtStart, adapter.lowestStackSize, adapter.uninitializedTypes),
typesFromAdapterStackRange(adapter.stack, adapter.lowestStackSize, adapter.uninitializedTypes),
info.startIndex,
info.getSize()
localMapFromAdapterLocalMap(adapter.localsRead, adapter.uninitializedTypes),
localMapFromAdapterLocalMap(adapter.localsWritten, adapter.uninitializedTypes),
typesFromAdapterStackRange(stackAtStart, adapter.lowestStackSize, adapter.uninitializedTypes),
typesFromAdapterStackRange(adapter.stack, adapter.lowestStackSize, adapter.uninitializedTypes),
info.startIndex,
info.getSize()
);
}
protected SortedMap<Integer, Type> localMapFromAdapterLocalMap(
SortedMap<Integer, Object> map, Map<Object, Object> uninitializedTypes) {
SortedMap<Integer, Object> map, Map<Object, Object> uninitializedTypes) {
SortedMap<Integer, Type> ret = new TreeMap<>();
map.forEach((k, v) -> ret.put(k, typeFromAdapterStackItem(v, uninitializedTypes)));
map.forEach((k, v) ->
{
if(v != Opcodes.TOP ) {
ret.put(k, typeFromAdapterStackItem(v, uninitializedTypes));
}
}
);
return ret;
}
protected List<Type> typesFromAdapterStackRange(
List<Object> stack, int start, Map<Object, Object> uninitializedTypes) {
List<Object> stack, int start, Map<Object, Object> uninitializedTypes) {
List<Type> ret = new ArrayList<>();
for (int i = start; i < stack.size(); i++) {
Object item = stack.get(i);

View File

@ -19,7 +19,7 @@ class Util {
static boolean isStoreOp(int opcode) {
return opcode == Opcodes.ISTORE || opcode == Opcodes.LSTORE || opcode == Opcodes.FSTORE ||
opcode == Opcodes.DSTORE || opcode == Opcodes.ASTORE;
opcode == Opcodes.DSTORE || opcode == Opcodes.ASTORE;
}
static int storeOpFromType(Type type) {
@ -55,13 +55,13 @@ class Util {
static void unboxStackIfNecessary(Type type, MethodNode method) {
if (type == Type.INT_TYPE) method.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/Integer", "intValue", Type.getMethodDescriptor(Type.INT_TYPE), false);
"java/lang/Integer", "intValue", Type.getMethodDescriptor(Type.INT_TYPE), false);
else if (type == Type.FLOAT_TYPE) method.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/Float", "floatValue", Type.getMethodDescriptor(Type.FLOAT_TYPE), false);
"java/lang/Float", "floatValue", Type.getMethodDescriptor(Type.FLOAT_TYPE), false);
else if (type == Type.LONG_TYPE) method.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/Long", "longValue", Type.getMethodDescriptor(Type.LONG_TYPE), false);
"java/lang/Long", "longValue", Type.getMethodDescriptor(Type.LONG_TYPE), false);
else if (type == Type.DOUBLE_TYPE) method.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/Double", "doubleValue", Type.getMethodDescriptor(Type.DOUBLE_TYPE), false);
"java/lang/Double", "doubleValue", Type.getMethodDescriptor(Type.DOUBLE_TYPE), false);
}
static AbstractInsnNode intConst(int v) {
@ -79,6 +79,6 @@ class Util {
static MethodInsnNode boxCall(Class<?> boxType, Type primType) {
return new MethodInsnNode(Opcodes.INVOKESTATIC, Type.getInternalName(boxType),
"valueOf", Type.getMethodDescriptor(Type.getType(boxType), primType), false);
"valueOf", Type.getMethodDescriptor(Type.getType(boxType), primType), false);
}
}

View File

@ -3,7 +3,19 @@ package asmble.ast
import java.util.*
import kotlin.reflect.KClass
/**
* All WebAssembly AST nodes as static inner classes.
*/
sealed class Node {
/**
* Wasm module definition.
*
* The unit of WebAssembly code is the module. A module collects definitions
* for types, functions, tables, memories, and globals. In addition, it can
* declare imports and exports and provide initialization logic in the form
* of data and element segments or a start function.
*/
data class Module(
val types: List<Type.Func> = emptyList(),
val imports: List<Import> = emptyList(),
@ -172,12 +184,15 @@ sealed class Node {
interface Const<out T : Number> : Args { val value: T }
}
// Control flow
// Control instructions [https://www.w3.org/TR/2018/WD-wasm-core-1-20180215/#control-instructions]
object Unreachable : Instr(), Args.None
object Nop : Instr(), Args.None
data class Block(override val type: Type.Value?) : Instr(), Args.Type
data class Loop(override val type: Type.Value?) : Instr(), Args.Type
data class If(override val type: Type.Value?) : Instr(), Args.Type
object Else : Instr(), Args.None
object End : Instr(), Args.None
data class Br(override val relativeDepth: Int) : Instr(), Args.RelativeDepth
@ -188,25 +203,27 @@ sealed class Node {
) : Instr(), Args.Table
object Return : Instr()
// Call operators
data class Call(override val index: Int) : Instr(), Args.Index
data class CallIndirect(
override val index: Int,
override val reserved: Boolean
) : Instr(), Args.ReservedIndex
// Parametric operators
// Parametric instructions [https://www.w3.org/TR/2018/WD-wasm-core-1-20180215/#parametric-instructions]
object Drop : Instr(), Args.None
object Select : Instr(), Args.None
// Variable access
// Variable instructions [https://www.w3.org/TR/2018/WD-wasm-core-1-20180215/#variable-instructions]
data class GetLocal(override val index: Int) : Instr(), Args.Index
data class SetLocal(override val index: Int) : Instr(), Args.Index
data class TeeLocal(override val index: Int) : Instr(), Args.Index
data class GetGlobal(override val index: Int) : Instr(), Args.Index
data class SetGlobal(override val index: Int) : Instr(), Args.Index
// Memory operators
// Memory instructions [https://www.w3.org/TR/2018/WD-wasm-core-1-20180215/#memory-instructions]
data class I32Load(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
data class I64Load(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
data class F32Load(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
@ -233,7 +250,9 @@ sealed class Node {
data class MemorySize(override val reserved: Boolean) : Instr(), Args.Reserved
data class MemoryGrow(override val reserved: Boolean) : Instr(), Args.Reserved
// Constants
// Numeric instructions [https://www.w3.org/TR/2018/WD-wasm-core-1-20180215/#numeric-instructions]
// Constants operators
data class I32Const(override val value: Int) : Instr(), Args.Const<Int>
data class I64Const(override val value: Long) : Instr(), Args.Const<Long>
data class F32Const(override val value: Float) : Instr(), Args.Const<Float>

View File

@ -2,10 +2,16 @@ package asmble.ast
import asmble.io.SExprToStr
/**
* Ast representation of wasm S-expressions (wast format).
* see [[https://webassembly.github.io/spec/core/text/index.html]]
*/
sealed class SExpr {
data class Multi(val vals: List<SExpr> = emptyList()) : SExpr() {
override fun toString() = SExprToStr.Compact.fromSExpr(this)
}
data class Symbol(
val contents: String = "",
val quoted: Boolean = false,
@ -15,4 +21,5 @@ sealed class SExpr {
// This is basically the same as the deprecated java.lang.String#getBytes
fun rawContentCharsToBytes() = contents.toCharArray().map(Char::toByte)
}
}

View File

@ -1,6 +1,10 @@
package asmble.ast
/**
* Ast representation of wasm script.
*/
data class Script(val commands: List<Cmd>) {
sealed class Cmd {
data class Module(val module: Node.Module, val name: String?): Cmd()
data class Register(val string: String, val name: String?): Cmd()

View File

@ -3,6 +3,9 @@ package asmble.cli
import asmble.compile.jvm.javaIdent
import asmble.run.jvm.Module
/**
* This class provide ''invoke'' WASM code functionality.
*/
open class Invoke : ScriptCommand<Invoke.Args>() {
override val name = "invoke"
@ -34,6 +37,7 @@ open class Invoke : ScriptCommand<Invoke.Args>() {
).also { bld.done() }
override fun run(args: Args) {
// Compiles wasm to bytecode, do registrations and so on.
val ctx = prepareContext(args.scriptArgs)
// Instantiate the module
val module =
@ -41,11 +45,11 @@ open class Invoke : ScriptCommand<Invoke.Args>() {
else ctx.registrations[args.module] as? Module.Instance ?:
error("Unable to find module registered as ${args.module}")
// Just make sure the module is instantiated here...
module.instance(ctx)
val instance = module.instance(ctx)
// If an export is provided, call it
if (args.export != "<start-func>") args.export.javaIdent.let { javaName ->
val method = module.cls.declaredMethods.find { it.name == javaName } ?:
error("Unable to find export '${args.export}'")
// Finds java method(wasm fn) in class(wasm module) by name(declared in <start-func>)
val method = module.cls.declaredMethods.find { it.name == javaName } ?: error("Unable to find export '${args.export}'")
// Map args to params
require(method.parameterTypes.size == args.args.size) {
"Given arg count of ${args.args.size} is invalid for $method"
@ -59,11 +63,20 @@ open class Invoke : ScriptCommand<Invoke.Args>() {
else -> error("Unrecognized type for param ${index + 1}: $paramType")
}
}
val result = method.invoke(module.instance(ctx), *params.toTypedArray())
val result = method.invoke(instance, *params.toTypedArray())
if (args.resultToStdout && method.returnType != Void.TYPE) println(result)
}
}
/**
* Arguments for 'invoke' command.
*
* @param scriptArgs Common arguments for 'invoke' and 'run' ScriptCommands.
* @param module The module name to run. If it's a JVM class, it must have a no-arg constructor
* @param export The specific export function to invoke
* @param args Parameter for the export if export is present
* @param resultToStdout If true result will print to stout
*/
data class Args(
val scriptArgs: ScriptCommand.ScriptArgs,
val module: String,

View File

@ -5,6 +5,9 @@ import kotlin.system.exitProcess
val commands = listOf(Compile, Help, Invoke, Link, Run, Translate)
/**
* Entry point of command line interface.
*/
fun main(args: Array<String>) {
if (args.isEmpty()) return println(
"""
@ -28,6 +31,7 @@ fun main(args: Array<String>) {
val globals = Main.globalArgs(argBuild)
logger = Logger.Print(globals.logLevel)
command.logger = logger
logger.info { "Running the command=${command.name} with args=${argBuild.args}" }
command.runWithArgs(argBuild)
} catch (e: Exception) {
logger.error { "Error ${command?.let { "in command '${it.name}'" } ?: ""}: ${e.message}" }

View File

@ -1,10 +1,10 @@
package asmble.cli
import asmble.ast.Script
import asmble.compile.jvm.javaIdent
import asmble.run.jvm.Module
import asmble.run.jvm.ScriptContext
import asmble.compile.jvm.*
import asmble.run.jvm.*
import java.io.File
import java.io.PrintWriter
import java.util.*
abstract class ScriptCommand<T> : Command<T>() {
@ -41,48 +41,97 @@ abstract class ScriptCommand<T> : Command<T>() {
desc = "The maximum number of memory pages when a module doesn't say.",
default = "5",
lowPriority = true
).toInt()
).toInt(),
enableLogger = bld.arg(
name = "enableLogger",
opt = "enableLogger",
desc = "Enables the special module the could be used for logging",
default = "false",
lowPriority = true
).toBoolean()
)
fun prepareContext(args: ScriptArgs): ScriptContext {
var ctx = ScriptContext(
var context = ScriptContext(
packageName = "asmble.temp" + UUID.randomUUID().toString().replace("-", ""),
defaultMaxMemPages = args.defaultMaxMemPages
defaultMaxMemPages = args.defaultMaxMemPages,
memoryBuilder = args.memoryBuilder
)
// Compile everything
ctx = args.inFiles.foldIndexed(ctx) { index, ctx, inFile ->
context = args.inFiles.foldIndexed(context) { index, ctx, inFile ->
try {
when (inFile.substringAfterLast('.')) {
// if input file is class file
"class" -> ctx.classLoader.addClass(File(inFile).readBytes()).let { ctx }
else -> Translate.inToAst(inFile, inFile.substringAfterLast('.')).let { inAst ->
val (mod, name) = (inAst.commands.singleOrNull() as? Script.Cmd.Module) ?:
// if input file is wasm file
else -> {
val translateCmd = Translate
translateCmd.logger = this.logger
translateCmd.inToAst(inFile, inFile.substringAfterLast('.')).let { inAst ->
val (mod, name) = (inAst.commands.singleOrNull() as? Script.Cmd.Module) ?:
error("Input file must only contain a single module")
val className = name?.javaIdent?.capitalize() ?:
val className = name?.javaIdent?.capitalize() ?:
"Temp" + UUID.randomUUID().toString().replace("-", "")
ctx.withCompiledModule(mod, className, name).let { ctx ->
if (name == null && index != args.inFiles.size - 1)
logger.warn { "File '$inFile' not last and has no name so will be unused" }
if (name == null || args.disableAutoRegister) ctx
else ctx.runCommand(Script.Cmd.Register(name, null))
ctx.withCompiledModule(mod, className, name).let { ctx ->
if (name == null && index != args.inFiles.size - 1)
logger.warn { "File '$inFile' not last and has no name so will be unused" }
if (name == null || args.disableAutoRegister) ctx
else ctx.runCommand(Script.Cmd.Register(name, null))
}
}
}
}
} catch (e: Exception) { throw Exception("Failed loading $inFile - ${e.message}", e) }
} catch (e: Exception) {
throw Exception("Failed loading $inFile - ${e.message}", e)
}
}
// Do registrations
ctx = args.registrations.fold(ctx) { ctx, (moduleName, className) ->
context = args.registrations.fold(context) { ctx, (moduleName, className) ->
ctx.withModuleRegistered(moduleName,
Module.Native(Class.forName(className, true, ctx.classLoader).newInstance()))
Module.Native(Class.forName(className, true, ctx.classLoader).newInstance()))
}
if (args.specTestRegister) ctx = ctx.withHarnessRegistered()
return ctx
if (args.specTestRegister) context = context.withHarnessRegistered()
if (args.enableLogger) {
// add logger Wasm module for logging
context =
context.withModuleRegistered(
"logger",
Module.Native(LoggerModule(PrintWriter(System.out)))
)
}
// add env Wasm module for gas metering
context =
context.withModuleRegistered(
"env",
// TODO: currently we are using almost infinite gas limit
Module.Native(EnvModule(Long.MAX_VALUE))
)
return context
}
/**
* Common arguments for 'invoke' and 'run' ScriptCommands.
*
* @param inFiles Files to add to classpath. Can be wasm, wast, or class file
* @param registrations Register class name to a module name
* @param disableAutoRegister If set, this will not auto-register modules with names
* @param specTestRegister If true, registers the spec test harness as 'spectest'
* @param defaultMaxMemPages The maximum number of memory pages when a module doesn't say
* @param enableLogger If set, the special logger module will be registred.
* @param memoryBuilder The builder to initialize new memory class.
*/
data class ScriptArgs(
val inFiles: List<String>,
val registrations: List<Pair<String, String>>,
val disableAutoRegister: Boolean,
val specTestRegister: Boolean,
val defaultMaxMemPages: Int
val defaultMaxMemPages: Int,
val enableLogger: Boolean,
val memoryBuilder: MemoryBufferBuilder? = null
)
}
}

View File

@ -58,7 +58,10 @@ open class Translate : Command<Translate.Args>() {
fun inToAst(inFile: String, inFormat: String): Script {
val inBytes =
if (inFile == "--") System.`in`.use { it.readBytes() }
else File(inFile).let { f -> FileInputStream(f).use { it.readBytes(f.length().toIntExact()) } }
else File(inFile).let { f ->
// Input file might not fit into the memory
FileInputStream(f).use { it.readBytes(f.length().toIntExact()) }
}
return when (inFormat) {
"wast" -> StrToSExpr.parse(inBytes.toString(Charsets.UTF_8)).let { res ->
when (res) {
@ -67,8 +70,10 @@ open class Translate : Command<Translate.Args>() {
}
}
"wasm" ->
Script(listOf(Script.Cmd.Module(BinaryToAst(logger = logger).toModule(
ByteReader.InputStream(inBytes.inputStream())), null)))
BinaryToAst(logger = this.logger).toModule(
ByteReader.InputStream(inBytes.inputStream())).let { module ->
Script(listOf(Script.Cmd.Module(module, module.names?.moduleName)))
}
else -> error("Unknown in format '$inFormat'")
}
}

View File

@ -13,7 +13,7 @@ import org.objectweb.asm.tree.ClassNode
open class AsmToBinary(val splitMethod: SplitMethod? = SplitMethod(Opcodes.ASM6)) {
fun fromClassNode(
cn: ClassNode,
newClassWriter: () -> ClassWriter = { ClassWriter(ClassWriter.COMPUTE_FRAMES + ClassWriter.COMPUTE_MAXS) }
newClassWriter: () -> ClassWriter = { ClassWriter(ClassWriter.COMPUTE_FRAMES) }
): ByteArray {
while (true) {
val cw = newClassWriter()
@ -29,6 +29,7 @@ open class AsmToBinary(val splitMethod: SplitMethod? = SplitMethod(Opcodes.ASM6)
require(cn.name == e.className)
val tooLargeIndex = cn.methods.indexOfFirst { it.name == e.methodName && it.desc == e.descriptor }
require(tooLargeIndex >= 0)
val tt = cn.methods[tooLargeIndex]
val split = splitMethod.split(cn.name, cn.methods[tooLargeIndex])
split ?: throw IllegalStateException("Failed to split", e)
// Change the split off method's name if there's already one

View File

@ -30,10 +30,11 @@ open class AstToAsm {
}
fun addFields(ctx: ClsContext) {
// Mem field if present
// Mem field if present, adds `private final field memory` to
if (ctx.hasMemory)
ctx.cls.fields.add(FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, "memory",
ctx.mem.memType.asmDesc, null, null))
ctx.cls.fields.add(
FieldNode((Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL), "memory", ctx.mem.memType.asmDesc, null, null)
)
// Table field if present...
// Private final for now, but likely won't be final in future versions supporting
// mutable tables, may be not even a table but a list (and final)

View File

@ -4,32 +4,30 @@ import asmble.ast.Node
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.tree.*
import java.nio.Buffer
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
open class ByteBufferMem(val direct: Boolean = true) : Mem {
override val memType = ByteBuffer::class.ref
open class ByteBufferMem : Mem {
override val memType: TypeRef = MemoryBuffer::class.ref
override fun limitAndCapacity(instance: Any) =
if (instance !is ByteBuffer) error("Unrecognized memory instance: $instance")
override fun limitAndCapacity(instance: Any): Pair<Int, Int> =
if (instance !is MemoryBuffer) error("Unrecognized memory instance: $instance")
else instance.limit() to instance.capacity()
override fun create(func: Func) = func.popExpecting(Int::class.ref).addInsns(
(if (direct) ByteBuffer::allocateDirect else ByteBuffer::allocate).invokeStatic()
(MemoryBuffer::init).invokeStatic()
).push(memType)
override fun init(func: Func, initial: Int) = func.popExpecting(memType).addInsns(
// Set the limit to initial
(initial * Mem.PAGE_SIZE).const,
forceFnType<ByteBuffer.(Int) -> Buffer>(ByteBuffer::limit).invokeVirtual(),
TypeInsnNode(Opcodes.CHECKCAST, ByteBuffer::class.ref.asmName),
forceFnType<MemoryBuffer.(Int) -> MemoryBuffer>(MemoryBuffer::limit).invokeVirtual(),
TypeInsnNode(Opcodes.CHECKCAST, memType.asmName),
// Set it to use little endian
ByteOrder::LITTLE_ENDIAN.getStatic(),
forceFnType<ByteBuffer.(ByteOrder) -> ByteBuffer>(ByteBuffer::order).invokeVirtual()
).push(ByteBuffer::class.ref)
forceFnType<MemoryBuffer.(ByteOrder) -> MemoryBuffer>(MemoryBuffer::order).invokeVirtual()
).push(memType)
override fun data(func: Func, bytes: ByteArray, buildOffset: (Func) -> Func) =
// Sadly there is no absolute bulk put, so we need to fake one. Ref:
@ -42,10 +40,10 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
// where we could call put directly, but it too is negligible for now.
// Note, with this approach, the mem not be left on the stack for future data() calls which is fine.
func.popExpecting(memType).
addInsns(ByteBuffer::duplicate.invokeVirtual()).
addInsns(MemoryBuffer::duplicate.invokeVirtual()).
let(buildOffset).popExpecting(Int::class.ref).
addInsns(
forceFnType<ByteBuffer.(Int) -> Buffer>(ByteBuffer::position).invokeVirtual(),
forceFnType<MemoryBuffer.(Int) -> MemoryBuffer>(MemoryBuffer::position).invokeVirtual(),
TypeInsnNode(Opcodes.CHECKCAST, memType.asmName)
).addInsns(
// We're going to do this as an LDC string in ISO-8859 and read it back at runtime. However,
@ -61,7 +59,7 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
"getBytes", "(Ljava/lang/String;)[B", false),
0.const,
bytes.size.const,
forceFnType<ByteBuffer.(ByteArray, Int, Int) -> ByteBuffer>(ByteBuffer::put).invokeVirtual()
forceFnType<MemoryBuffer.(ByteArray, Int, Int) -> MemoryBuffer>(MemoryBuffer::put).invokeVirtual()
)
}.toList()
).addInsns(
@ -69,7 +67,7 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
)
override fun currentMemory(ctx: FuncContext, func: Func) = func.popExpecting(memType).addInsns(
forceFnType<ByteBuffer.() -> Int>(ByteBuffer::limit).invokeVirtual(),
forceFnType<MemoryBuffer.() -> Int>(MemoryBuffer::limit).invokeVirtual(),
Mem.PAGE_SIZE.const,
InsnNode(Opcodes.IDIV)
).push(Int::class.ref)
@ -86,10 +84,10 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
val okLim = LabelNode()
val node = MethodNode(
Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_SYNTHETIC,
"\$\$growMemory", "(Ljava/nio/ByteBuffer;I)I", null, null
"\$\$growMemory", "(Lasmble/compile/jvm/MemoryBuffer;I)I", null, null
).addInsns(
VarInsnNode(Opcodes.ALOAD, 0), // [mem]
forceFnType<ByteBuffer.() -> Int>(ByteBuffer::limit).invokeVirtual(), // [lim]
forceFnType<MemoryBuffer.() -> Int>(MemoryBuffer::limit).invokeVirtual(), // [lim]
InsnNode(Opcodes.DUP), // [lim, lim]
VarInsnNode(Opcodes.ALOAD, 0), // [lim, lim, mem]
InsnNode(Opcodes.SWAP), // [lim, mem, lim]
@ -102,7 +100,7 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
InsnNode(Opcodes.LADD), // [lim, mem, newlimL]
InsnNode(Opcodes.DUP2), // [lim, mem, newlimL, newlimL]
VarInsnNode(Opcodes.ALOAD, 0), // [lim, mem, newlimL, newlimL, mem]
ByteBuffer::capacity.invokeVirtual(), // [lim, mem, newlimL, newlimL, cap]
MemoryBuffer::capacity.invokeVirtual(), // [lim, mem, newlimL, newlimL, cap]
InsnNode(Opcodes.I2L), // [lim, mem, newlimL, newlimL, capL]
InsnNode(Opcodes.LCMP), // [lim, mem, newlimL, cmpres]
JumpInsnNode(Opcodes.IFLE, okLim), // [lim, mem, newlimL]
@ -111,7 +109,7 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
InsnNode(Opcodes.IRETURN),
okLim, // [lim, mem, newlimL]
InsnNode(Opcodes.L2I), // [lim, mem, newlim]
forceFnType<ByteBuffer.(Int) -> Buffer>(ByteBuffer::limit).invokeVirtual(), // [lim, mem]
forceFnType<MemoryBuffer.(Int) -> MemoryBuffer>(MemoryBuffer::limit).invokeVirtual(), // [lim, mem]
InsnNode(Opcodes.POP), // [lim]
Mem.PAGE_SIZE.const, // [lim, pagesize]
InsnNode(Opcodes.IDIV), // [limpages]
@ -125,7 +123,7 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
// Ug, some tests expect this to be a runtime failure so we feature flagged it
if (ctx.cls.eagerFailLargeMemOffset)
require(insn.offset <= Int.MAX_VALUE, { "Offsets > ${Int.MAX_VALUE} unsupported" }).let { this }
fun Func.load(fn: ByteBuffer.(Int) -> Any, retClass: KClass<*>) =
fun Func.load(fn: MemoryBuffer.(Int) -> Any, retClass: KClass<*>) =
this.popExpecting(Int::class.ref).let { func ->
// No offset means we'll access it directly
(if (insn.offset == 0L) func else {
@ -141,9 +139,9 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
}
}).popExpecting(memType).addInsns((fn as KFunction<*>).invokeVirtual())
}.push(retClass.ref)
fun Func.loadI32(fn: ByteBuffer.(Int) -> Any) =
fun Func.loadI32(fn: MemoryBuffer.(Int) -> Any) =
this.load(fn, Int::class)
fun Func.loadI64(fn: ByteBuffer.(Int) -> Any) =
fun Func.loadI64(fn: MemoryBuffer.(Int) -> Any) =
this.load(fn, Long::class)
/* Ug: https://youtrack.jetbrains.com/issue/KT-17064
fun Func.toUnsigned(fn: KFunction<*>) =
@ -163,33 +161,33 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
// Had to move this in here instead of as first expr because of https://youtrack.jetbrains.com/issue/KT-8689
return when (insn) {
is Node.Instr.I32Load ->
func.loadI32(ByteBuffer::getInt)
func.loadI32(MemoryBuffer::getInt)
is Node.Instr.I64Load ->
func.loadI64(ByteBuffer::getLong)
func.loadI64(MemoryBuffer::getLong)
is Node.Instr.F32Load ->
func.load(ByteBuffer::getFloat, Float::class)
func.load(MemoryBuffer::getFloat, Float::class)
is Node.Instr.F64Load ->
func.load(ByteBuffer::getDouble, Double::class)
func.load(MemoryBuffer::getDouble, Double::class)
is Node.Instr.I32Load8S ->
func.loadI32(ByteBuffer::get)
func.loadI32(MemoryBuffer::get)
is Node.Instr.I32Load8U ->
func.loadI32(ByteBuffer::get).toUnsigned32(java.lang.Byte::class, "toUnsignedInt", Byte::class)
func.loadI32(MemoryBuffer::get).toUnsigned32(java.lang.Byte::class, "toUnsignedInt", Byte::class)
is Node.Instr.I32Load16S ->
func.loadI32(ByteBuffer::getShort)
func.loadI32(MemoryBuffer::getShort)
is Node.Instr.I32Load16U ->
func.loadI32(ByteBuffer::getShort).toUnsigned32(java.lang.Short::class, "toUnsignedInt", Short::class)
func.loadI32(MemoryBuffer::getShort).toUnsigned32(java.lang.Short::class, "toUnsignedInt", Short::class)
is Node.Instr.I64Load8S ->
func.loadI32(ByteBuffer::get).i32ToI64()
func.loadI32(MemoryBuffer::get).i32ToI64()
is Node.Instr.I64Load8U ->
func.loadI32(ByteBuffer::get).toUnsigned64(java.lang.Byte::class, "toUnsignedLong", Byte::class)
func.loadI32(MemoryBuffer::get).toUnsigned64(java.lang.Byte::class, "toUnsignedLong", Byte::class)
is Node.Instr.I64Load16S ->
func.loadI32(ByteBuffer::getShort).i32ToI64()
func.loadI32(MemoryBuffer::getShort).i32ToI64()
is Node.Instr.I64Load16U ->
func.loadI32(ByteBuffer::getShort).toUnsigned64(java.lang.Short::class, "toUnsignedLong", Short::class)
func.loadI32(MemoryBuffer::getShort).toUnsigned64(java.lang.Short::class, "toUnsignedLong", Short::class)
is Node.Instr.I64Load32S ->
func.loadI32(ByteBuffer::getInt).i32ToI64()
func.loadI32(MemoryBuffer::getInt).i32ToI64()
is Node.Instr.I64Load32U ->
func.loadI32(ByteBuffer::getInt).toUnsigned64(java.lang.Integer::class, "toUnsignedLong", Int::class)
func.loadI32(MemoryBuffer::getInt).toUnsigned64(java.lang.Integer::class, "toUnsignedLong", Int::class)
else -> throw IllegalArgumentException("Unknown load op $insn")
}
}
@ -224,12 +222,12 @@ open class ByteBufferMem(val direct: Boolean = true) : Mem {
popExpecting(Int::class.ref).
popExpecting(memType).
addInsns(fn).
push(ByteBuffer::class.ref)
push(memType)
}
// Ug, I hate these as strings but can't introspect Kotlin overloads
fun bufStoreFunc(name: String, valType: KClass<*>) =
MethodInsnNode(Opcodes.INVOKEVIRTUAL, ByteBuffer::class.ref.asmName, name,
ByteBuffer::class.ref.asMethodRetDesc(Int::class.ref, valType.ref), false)
MethodInsnNode(Opcodes.INVOKEVIRTUAL, memType.asmName, name,
memType.asMethodRetDesc(Int::class.ref, valType.ref), false)
fun Func.changeI64ToI32() =
this.popExpecting(Long::class.ref).push(Int::class.ref)
when (insn) {

View File

@ -4,6 +4,24 @@ import asmble.ast.Node
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.*
/**
* Jvm representation of a function.
*
* @param name Name of the fn.
* @param params List of parameters of the fn.
* @param ret Type of the fn returner value.
* @param access The value of the access_flags item is a mask of flags used to
* denote access permissions to and properties of this class or
* interface [https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1-200-E.1].
* @param insns List of nodes that represents a bytecode instruction.
* @param stack A stack of operand types. Mirror of the operand stack(jvm stack)
* where types of operands instead operands.
* @param blockStack List of blocks of code
* @param ifStack Contains index of [org.objectweb.asm.tree.JumpInsnNode] that
* has a null label initially
* @param lastStackIsMemLeftover If there is the memory on the stack and we need it
* in the future, we mark it as leftover and reuse
*/
data class Func(
val name: String,
val params: List<TypeRef> = emptyList(),
@ -12,7 +30,6 @@ data class Func(
val insns: List<AbstractInsnNode> = emptyList(),
val stack: List<TypeRef> = emptyList(),
val blockStack: List<Block> = emptyList(),
// Contains index of JumpInsnNode that has a null label initially
val ifStack: List<Int> = emptyList(),
val lastStackIsMemLeftover: Boolean = false
) {
@ -110,10 +127,11 @@ data class Func(
}
}
fun pushBlock(insn: Node.Instr, labelType: Node.Type.Value?, endType: Node.Type.Value?) =
/** Creates new block with specified instruction and pushes it into the blockStack.*/
fun pushBlock(insn: Node.Instr, labelType: Node.Type.Value?, endType: Node.Type.Value?): Func =
pushBlock(insn, listOfNotNull(labelType?.typeRef), listOfNotNull(endType?.typeRef))
fun pushBlock(insn: Node.Instr, labelTypes: List<TypeRef>, endTypes: List<TypeRef>) =
fun pushBlock(insn: Node.Instr, labelTypes: List<TypeRef>, endTypes: List<TypeRef>): Func =
copy(blockStack = blockStack + Block(insn, insns.size, stack, labelTypes, endTypes))
fun popBlock() = copy(blockStack = blockStack.dropLast(1)) to blockStack.last()
@ -127,6 +145,22 @@ data class Func(
fun popIf() = copy(ifStack = ifStack.dropLast(1)) to peekIf()
/**
* Representation of code block.
*
* Blocks are composed of matched pairs of `block ... end` instructions, loops
* with matched pairs of `loop ... end` instructions, and ifs with either
* `if ... end` or if ... else ... end sequences. For each of these constructs
* the instructions in the ellipsis are said to be enclosed in the construct.
*
* @param isns Start instruction of this block, might be a 'Block', 'Loop'
* or 'If'
* @param startIndex Index of start instruction of this block in list of all
* instructions
* @param origStack Current block stack of operand types.
* @param labelTypes A type of label for this block
* @param endTypes A type of block return value
*/
class Block(
val insn: Node.Instr,
val startIndex: Int,

View File

@ -14,23 +14,31 @@ import java.lang.invoke.MethodHandle
// TODO: modularize
open class FuncBuilder {
fun fromFunc(ctx: ClsContext, f: Node.Func, index: Int): Func {
/**
* Converts wasm AST [asmble.ast.Node.Func] to Jvm bytecode representation [asmble.compile.jvm.Func].
*
* @param ctx A Global context for converting.
* @param fn AST of wasm fn.
* @param index Fn index, used for generating fn name
*/
fun fromFunc(ctx: ClsContext, fn: Node.Func, index: Int): Func {
ctx.debug { "Building function ${ctx.funcName(index)}" }
ctx.trace { "Function ast:\n${SExprToStr.fromSExpr(AstToSExpr.fromFunc(f))}" }
ctx.trace { "Function ast:\n${SExprToStr.fromSExpr(AstToSExpr.fromFunc(fn))}" }
var func = Func(
access = Opcodes.ACC_PRIVATE,
name = ctx.funcName(index),
params = f.type.params.map(Node.Type.Value::typeRef),
ret = f.type.ret?.let(Node.Type.Value::typeRef) ?: Void::class.ref
params = fn.type.params.map(Node.Type.Value::typeRef),
ret = fn.type.ret?.let(Node.Type.Value::typeRef) ?: Void::class.ref
)
// Rework the instructions
val reworkedInsns = ctx.reworker.rework(ctx, f)
val reworkedInsns = ctx.reworker.rework(ctx, fn)
// Start the implicit block
func = func.pushBlock(Node.Instr.Block(f.type.ret), f.type.ret, f.type.ret)
func = func.pushBlock(Node.Instr.Block(fn.type.ret), fn.type.ret, fn.type.ret)
// Create the context
val funcCtx = FuncContext(
cls = ctx,
node = f,
node = fn,
insns = reworkedInsns,
memIsLocalVar =
ctx.reworker.nonAdjacentMemAccesses(reworkedInsns) >= ctx.nonAdjacentMemAccessesRequiringLocalVar
@ -46,9 +54,9 @@ open class FuncBuilder {
// Add all instructions
ctx.debug { "Applying insns for function ${ctx.funcName(index)}" }
// All functions have an implicit block
func = funcCtx.insns.foldIndexed(func) { index, func, insn ->
func = funcCtx.insns.foldIndexed(func) { idx, f, insn ->
ctx.debug { "Applying insn $insn" }
val ret = applyInsn(funcCtx, func, insn, index)
val ret = applyInsn(funcCtx, f, insn, idx)
ctx.trace { "Resulting stack: ${ret.stack}"}
ret
}
@ -56,11 +64,11 @@ open class FuncBuilder {
// End the implicit block
val implicitBlock = func.currentBlock
func = applyEnd(funcCtx, func)
f.type.ret?.typeRef?.also { func = func.popExpecting(it, implicitBlock) }
fn.type.ret?.typeRef?.also { func = func.popExpecting(it, implicitBlock) }
// If the last instruction does not terminate, add the expected return
if (func.insns.isEmpty() || !func.insns.last().isTerminating) {
func = func.addInsns(InsnNode(when (f.type.ret) {
func = func.addInsns(InsnNode(when (fn.type.ret) {
null -> Opcodes.RETURN
Node.Type.Value.I32 -> Opcodes.IRETURN
Node.Type.Value.I64 -> Opcodes.LRETURN
@ -72,8 +80,10 @@ open class FuncBuilder {
}
fun applyInsn(ctx: FuncContext, fn: Func, i: Insn, index: Int) = when (i) {
is Insn.Node ->
applyNodeInsn(ctx, fn, i.insn, index)
is Insn.ImportFuncRefNeededOnStack ->
// Func refs are method handle fields
fn.addInsns(
@ -81,6 +91,7 @@ open class FuncBuilder {
FieldInsnNode(Opcodes.GETFIELD, ctx.cls.thisRef.asmName,
ctx.cls.funcName(i.index), MethodHandle::class.ref.asmDesc)
).push(MethodHandle::class.ref)
is Insn.ImportGlobalSetRefNeededOnStack ->
// Import setters are method handle fields
fn.addInsns(
@ -88,13 +99,17 @@ open class FuncBuilder {
FieldInsnNode(Opcodes.GETFIELD, ctx.cls.thisRef.asmName,
ctx.cls.importGlobalSetterFieldName(i.index), MethodHandle::class.ref.asmDesc)
).push(MethodHandle::class.ref)
is Insn.ThisNeededOnStack ->
// load a reference onto the stack from a local variable
fn.addInsns(VarInsnNode(Opcodes.ALOAD, 0)).push(ctx.cls.thisRef)
is Insn.MemNeededOnStack ->
putMemoryOnStack(ctx, fn)
}
fun applyNodeInsn(ctx: FuncContext, fn: Func, i: Node.Instr, index: Int) = when (i) {
is Node.Instr.Unreachable ->
fn.addInsns(UnsupportedOperationException::class.athrow("Unreachable")).markUnreachable()
is Node.Instr.Nop ->
@ -127,18 +142,16 @@ open class FuncBuilder {
fn.pop().let { (fn, popped) ->
fn.addInsns(InsnNode(if (popped.stackSize == 2) Opcodes.POP2 else Opcodes.POP))
}
is Node.Instr.Select ->
applySelectInsn(ctx, fn)
is Node.Instr.GetLocal ->
applyGetLocal(ctx, fn, i.index)
is Node.Instr.SetLocal ->
applySetLocal(ctx, fn, i.index)
is Node.Instr.TeeLocal ->
applyTeeLocal(ctx, fn, i.index)
is Node.Instr.GetGlobal ->
applyGetGlobal(ctx, fn, i.index)
is Node.Instr.SetGlobal ->
applySetGlobal(ctx, fn, i.index)
is Node.Instr.Select -> applySelectInsn(ctx, fn)
// Variable access
is Node.Instr.GetLocal -> applyGetLocal(ctx, fn, i.index)
is Node.Instr.SetLocal -> applySetLocal(ctx, fn, i.index)
is Node.Instr.TeeLocal -> applyTeeLocal(ctx, fn, i.index)
is Node.Instr.GetGlobal -> applyGetGlobal(ctx, fn, i.index)
is Node.Instr.SetGlobal -> applySetGlobal(ctx, fn, i.index)
// Memory operators
is Node.Instr.I32Load, is Node.Instr.I64Load, is Node.Instr.F32Load, is Node.Instr.F64Load,
is Node.Instr.I32Load8S, is Node.Instr.I32Load8U, is Node.Instr.I32Load16U, is Node.Instr.I32Load16S,
is Node.Instr.I64Load8S, is Node.Instr.I64Load8U, is Node.Instr.I64Load16U, is Node.Instr.I64Load16S,
@ -461,18 +474,18 @@ open class FuncBuilder {
fun popUntilStackSize(
ctx: FuncContext,
fn: Func,
func: Func,
block: Func.Block,
untilStackSize: Int,
keepLast: Boolean
): Func {
ctx.debug { "For block ${block.insn}, popping until stack size $untilStackSize, keeping last? $keepLast" }
// Just get the latest, don't actually pop...
val type = if (keepLast) fn.pop().second else null
return (0 until Math.max(0, fn.stack.size - untilStackSize)).fold(fn) { fn, _ ->
val type = if (keepLast) func.pop().second else null
return (0 until Math.max(0, func.stack.size - untilStackSize)).fold(func) { fn, _ ->
// Essentially swap and pop if they want to keep the latest
(if (type != null && fn.stack.size > 1) fn.stackSwap(block) else fn).let { fn ->
fn.pop(block).let { (fn, poppedType) ->
(if (type != null && fn.stack.size > 1) fn.stackSwap(block) else fn).let { f ->
f.pop(block).let { (fn, poppedType) ->
fn.addInsns(InsnNode(if (poppedType.stackSize == 2) Opcodes.POP2 else Opcodes.POP))
}
}
@ -1076,10 +1089,12 @@ open class FuncBuilder {
putMemoryOnStack(ctx, fn).let { fn -> ctx.cls.mem.currentMemory(ctx, fn) }
}
/**
* Store is a special case where the memory ref is already pre-injected on
* the stack before this call. But it can have a memory leftover on the stack
* so we pop it if we need to
*/
fun applyStoreOp(ctx: FuncContext, fn: Func, insn: Node.Instr.Args.AlignOffset, insnIndex: Int) =
// Store is a special case where the memory ref is already pre-injected on
// the stack before this call. But it can have a memory leftover on the stack
// so we pop it if we need to
ctx.cls.assertHasMemory().let {
ctx.cls.mem.storeOp(ctx, fn, insn).let { fn ->
// As a special case, if this leaves the mem on the stack
@ -1268,11 +1283,11 @@ open class FuncBuilder {
}
}
fun applyReturnInsn(ctx: FuncContext, fn: Func): Func {
// If the current stakc is unreachable, we consider that our block since it
fun applyReturnInsn(ctx: FuncContext, func: Func): Func {
// If the current stack is unreachable, we consider that our block since it
// will pop properly.
val block = if (fn.currentBlock.unreachable) fn.currentBlock else fn.blockStack.first()
popForBlockEscape(ctx, fn, block).let { fn ->
val block = if (func.currentBlock.unreachable) func.currentBlock else func.blockStack.first()
popForBlockEscape(ctx, func, block).let { fn ->
return when (ctx.node.type.ret) {
null ->
fn.addInsns(InsnNode(Opcodes.RETURN))
@ -1284,9 +1299,9 @@ open class FuncBuilder {
fn.popExpecting(Float::class.ref, block).addInsns(InsnNode(Opcodes.FRETURN))
Node.Type.Value.F64 ->
fn.popExpecting(Double::class.ref, block).addInsns(InsnNode(Opcodes.DRETURN))
}.let { fn ->
if (fn.stack.isNotEmpty()) throw CompileErr.UnusedStackOnReturn(fn.stack)
fn.markUnreachable()
}.let { it ->
if (it.stack.isNotEmpty()) throw CompileErr.UnusedStackOnReturn(it.stack)
it.markUnreachable()
}
}
}

View File

@ -3,6 +3,15 @@ package asmble.compile.jvm
import asmble.ast.Node
import asmble.util.Logger
/**
* Jvm context of execution a function.
*
* @param cls Class execution context
* @param node Ast of this function
* @param insns A list of instructions
* @param memIsLocalVar If true then function use only local variables and don't load
* and store from memory.
*/
data class FuncContext(
val cls: ClsContext,
val node: Node.Func,

View File

@ -2,6 +2,9 @@ package asmble.compile.jvm
import asmble.ast.Node
/**
* Does some special manipulations with instruction.
*/
open class InsnReworker {
fun rework(ctx: ClsContext, func: Node.Func): List<Insn> {
@ -29,6 +32,7 @@ open class InsnReworker {
// Note, while walking backwards up the insns to find set/tee, we do skip entire
// blocks/loops/if+else combined with "end"
var neededEagerLocalIndices = emptySet<Int>()
fun addEagerSetIfNeeded(getInsnIndex: Int, localIndex: Int) {
// Within the param range? nothing needed
if (localIndex < func.type.params.size) return
@ -71,6 +75,7 @@ open class InsnReworker {
(insn is Insn.Node && insn.insn !is Node.Instr.SetLocal && insn.insn !is Node.Instr.TeeLocal)
if (needsEagerInit) neededEagerLocalIndices += localIndex
}
insns.forEachIndexed { index, insn ->
if (insn is Insn.Node && insn.insn is Node.Instr.GetLocal) addEagerSetIfNeeded(index, insn.insn.index)
}
@ -86,11 +91,18 @@ open class InsnReworker {
} + insns
}
/**
* Puts into instruction list needed instructions for pushing local variables
* into the stack and returns list of resulting instructions.
*
* @param ctx The Execution context
* @param insns The original instructions
*/
fun injectNeededStackVars(ctx: ClsContext, insns: List<Node.Instr>): List<Insn> {
ctx.trace { "Calculating places to inject needed stack variables" }
// How we do this:
// We run over each insn, and keep a running list of stack
// manips. If there is an insn that needs something so far back,
// manips. If there is an ins'n that needs something so far back,
// we calc where it needs to be added and keep a running list of
// insn inserts. Then at the end we settle up.
//
@ -109,6 +121,14 @@ open class InsnReworker {
// guarantee the value will be in the right order if there are
// multiple for the same index
var insnsToInject = emptyMap<Int, List<Insn>>()
/**
* This function inject current instruction in stack.
*
* @param insn The instruction to inject
* @param count Number of step back on the stack that should we do for
* finding injection index.
*/
fun injectBeforeLastStackCount(insn: Insn, count: Int) {
ctx.trace { "Injecting $insn back $count stack values" }
fun inject(index: Int) {
@ -140,9 +160,11 @@ open class InsnReworker {
}
countSoFar += amountChanged
if (!foundUnconditionalJump) foundUnconditionalJump = insns[insnIndex].let { insn ->
insn is Node.Instr.Br || insn is Node.Instr.BrTable ||
insn is Node.Instr.Unreachable || insn is Node.Instr.Return
if (!foundUnconditionalJump) {
foundUnconditionalJump = insns[insnIndex].let { insn ->
insn is Node.Instr.Br || insn is Node.Instr.BrTable ||
insn is Node.Instr.Unreachable || insn is Node.Instr.Return
}
}
if (countSoFar == count) {
ctx.trace { "Found injection point as before insn #$insnIndex" }
@ -205,13 +227,19 @@ open class InsnReworker {
}
// Build resulting list
return insns.foldIndexed(emptyList<Insn>()) { index, ret, insn ->
return insns.foldIndexed(emptyList()) { index, ret, insn ->
val injections = insnsToInject[index] ?: emptyList()
ret + injections + Insn.Node(insn)
}
}
fun insnStackDiff(ctx: ClsContext, insn: Node.Instr) = when (insn) {
/**
* Calculate stack difference after calling instruction current instruction.
* Returns the difference from stack cursor position before instruction and after.
* `N = PUSH_OPS - POP_OPS.` '-n' mean that POP operation will be more than PUSH.
* If '0' then stack won't changed.
*/
fun insnStackDiff(ctx: ClsContext, insn: Node.Instr): Int = when (insn) {
is Node.Instr.Unreachable, is Node.Instr.Nop, is Node.Instr.Block,
is Node.Instr.Loop, is Node.Instr.Else, is Node.Instr.End, is Node.Instr.Br,
is Node.Instr.Return -> NOP
@ -283,7 +311,8 @@ open class InsnReworker {
is Node.Instr.F64ReinterpretI64 -> POP_PARAM + PUSH_RESULT
}
fun nonAdjacentMemAccesses(insns: List<Insn>) = insns.fold(0 to false) { (count, lastCouldHaveMem), insn ->
/** Returns number of memory accesses. */
fun nonAdjacentMemAccesses(insns: List<Insn>): Int = insns.fold(0 to false) { (count, lastCouldHaveMem), insn ->
val inc =
if (lastCouldHaveMem) 0
else if (insn == Insn.MemNeededOnStack) 1

View File

@ -0,0 +1,122 @@
package asmble.compile.jvm
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* The default implementation of MemoryBuffer that based on java.nio.ByteBuffer
*/
open class MemoryByteBuffer(val bb: ByteBuffer) : MemoryBuffer() {
override fun put(arr: ByteArray): MemoryBuffer {
bb.put(arr)
return this
}
override fun clear(): MemoryBuffer {
bb.clear()
return this
}
override fun get(arr: ByteArray): MemoryBuffer {
bb.get(arr)
return this
}
override fun putLong(index: Int, n: Long): MemoryBuffer {
bb.putLong(index, n)
return this
}
override fun putDouble(index: Int, n: Double): MemoryBuffer {
bb.putDouble(index, n)
return this
}
override fun putShort(index: Int, n: Short): MemoryBuffer {
bb.putShort(index, n)
return this
}
override fun putFloat(index: Int, n: Float): MemoryBuffer {
bb.putFloat(index, n)
return this
}
override fun put(index: Int, b: Byte): MemoryBuffer {
bb.put(index, b)
return this
}
override fun putInt(index: Int, n: Int): MemoryBuffer {
bb.putInt(index, n)
return this
}
override fun capacity(): Int {
return bb.capacity()
}
override fun limit(): Int {
return bb.limit()
}
override fun limit(newLimit: Int): MemoryBuffer {
bb.limit(newLimit)
return this
}
override fun position(newPosition: Int): MemoryBuffer {
bb.position(newPosition)
return this
}
override fun order(order: ByteOrder): MemoryBuffer {
bb.order(order)
return this
}
override fun duplicate(): MemoryBuffer {
return MemoryByteBuffer(bb.duplicate())
}
override fun put(arr: ByteArray, offset: Int, length: Int): MemoryBuffer {
bb.put(arr, offset, length)
return this
}
override fun getInt(index: Int): Int {
return bb.getInt(index)
}
override fun get(index: Int): Byte {
return bb.get(index)
}
override fun getLong(index: Int): Long {
return bb.getLong(index)
}
override fun getShort(index: Int): Short {
return bb.getShort(index)
}
override fun getFloat(index: Int): Float {
return bb.getFloat(index)
}
override fun getDouble(index: Int): Double {
return bb.getDouble(index)
}
override fun equals(other: Any?): Boolean {
if (this === other)
return true
if (other !is MemoryByteBuffer)
return false
return bb == other.bb
}
override fun hashCode(): Int {
return bb.hashCode()
}
}

View File

@ -2,14 +2,25 @@ package asmble.compile.jvm
import org.objectweb.asm.Type
/**
* A Java field or method type. This class can be used to make it easier to
* manipulate type and method descriptors.
*
* @param asm Wrapped [org.objectweb.asm.Type] from asm library
*/
data class TypeRef(val asm: Type) {
/** The internal name of the class corresponding to this object or array type. */
val asmName: String get() = asm.internalName
/** The descriptor corresponding to this Java type. */
val asmDesc: String get() = asm.descriptor
fun asMethodRetDesc(vararg args: TypeRef) = Type.getMethodDescriptor(asm, *args.map { it.asm }.toTypedArray())
/** Size of this type in stack, either 1 or 2 only allowed, where 1 = 2^32` bits */
val stackSize: Int get() = if (asm == Type.DOUBLE_TYPE || asm == Type.LONG_TYPE) 2 else 1
fun asMethodRetDesc(vararg args: TypeRef) = Type.getMethodDescriptor(asm, *args.map { it.asm }.toTypedArray())
fun equivalentTo(other: TypeRef) = this == other || this == Unknown || other == Unknown
object UnknownType

View File

@ -2,12 +2,11 @@ package asmble.io
import asmble.ast.Node
import asmble.util.*
import java.io.ByteArrayInputStream
import java.nio.ByteBuffer
open class BinaryToAst(
val version: Long = 1L,
val logger: Logger = Logger.Print(Logger.Level.OFF),
val logger: Logger = Logger.Print(Logger.Level.WARN),
val includeNameSection: Boolean = true
) : Logger by logger {
@ -186,12 +185,14 @@ open class BinaryToAst(
require(sectionId > maxSectionId) { "Section ID $sectionId came after $maxSectionId" }.
also { maxSectionId = sectionId }
val sectionLen = b.readVarUInt32AsInt()
// each 'read' invocation creates new InputStream and don't closes it
sections += sectionId to b.read(sectionLen)
}
// Now build the module
fun <T> readSectionList(sectionId: Int, fn: (ByteReader) -> T) =
sections.find { it.first == sectionId }?.second?.readList(fn) ?: emptyList()
val types = readSectionList(1, this::toFuncType)
val funcIndices = readSectionList(3) { it.readVarUInt32AsInt() }
var nameSection: Node.NameSection? = null

View File

@ -100,6 +100,7 @@ abstract class ByteReader {
return result
}
// todo looks like this InputStream isn't ever closed
class InputStream(val ins: java.io.InputStream) : ByteReader() {
private var nextByte: Byte? = null
private var sawEof = false

View File

@ -0,0 +1,56 @@
package asmble.run.jvm
/**
* Used to tack the state of the environment module.
*/
data class EnvState(
var spentGas: Long = 0,
// executed instruction counter
var EIC: Long = 0
)
/**
* Module used for gas and EIC metering.
*/
open class EnvModule(private val gasLimit: Long) {
private var state = EnvState();
/**
* [Wasm function]
* Adds spent gas to overall spent gas and checks limit exceeding.
*/
fun gas(spentGas: Int) {
if(state.spentGas + spentGas > gasLimit) {
// TODO : check for overflow, throw an exception
}
state.spentGas += spentGas;
}
/**
* [Wasm function]
* Adds EIC to overall executed instruction counter.
*/
fun eic(EIC: Int) {
state.EIC += EIC;
}
/**
* Sets spent gas and EIC value to 0. Used from WasmVm to clear gas value before metering.
* It should be impossible to call this function from a Wasm module.
*/
fun clearState() {
state.spentGas = 0;
state.EIC = 0;
}
/**
* Returns environment module state.
* Used from WasmVm to determine spent gas and executed instruction counter after each invocation.
*/
fun getState(): EnvState {
return state;
}
}

View File

@ -0,0 +1,45 @@
package asmble.run.jvm
import asmble.compile.jvm.Mem
import java.io.PrintWriter
import java.nio.ByteBuffer
/**
* Module used for logging UTF-8 strings from a Wasm module to a given writer.
*/
open class LoggerModule(val writer: PrintWriter) {
// one memory page is quite enough for save temporary buffer
private val memoryPages = 1
private val memory =
ByteBuffer.allocate(memoryPages * Mem.PAGE_SIZE) as ByteBuffer
/**
* [Wasm function]
* Writes one byte to the logger memory buffer. If there is no place flushes
* all data from the buffer to [PrintWriter] and try to put the byte again.
*/
fun write(byte: Int) {
val isFull = memory.position() >= memory.limit()
if (isFull) {
flush()
}
memory.put(byte.toByte())
}
/**
* [Wasm function]
* Reads all bytes from the logger memory buffer, convert its to UTF-8
* string and writes to stdout.
* Cleans the logger memory buffer.
*/
fun flush() {
val message = String(memory.array(), 0, memory.position())
writer.print(message)
writer.flush()
memory.clear()
}
}

View File

@ -76,6 +76,8 @@ interface Module {
// If there is a memory import, we have to get the one with the mem class as the first
val memImport = mod.imports.find { it.kind is Node.Import.Kind.Memory }
val builder = ctx.memoryBuilder
val memLimit = if (memImport != null) {
constructor = cls.declaredConstructors.find { it.parameterTypes.firstOrNull()?.ref == mem.memType }
val memImportKind = memImport.kind as Node.Import.Kind.Memory
@ -89,6 +91,13 @@ interface Module {
throw RunErr.ImportMemoryCapacityTooLarge(it * Mem.PAGE_SIZE, memCap)
}
memLimit
} else if (builder != null) {
constructor = cls.declaredConstructors.find { it.parameterTypes.firstOrNull()?.ref == mem.memType }
val memLimit = ctx.defaultMaxMemPages * Mem.PAGE_SIZE
val memInst = builder.build(memLimit)
constructorParams += memInst
memLimit
} else {
// Find the constructor with no max mem amount (i.e. not int and not memory)
constructor = cls.declaredConstructors.find {

View File

@ -19,6 +19,20 @@ import java.lang.invoke.MethodType
import java.lang.reflect.InvocationTargetException
import java.util.*
/**
* Script context. Contains all the information needed to execute this script.
*
* @param packageName Package name for this script
* @param modules List of all modules to be load for this script
* @param registrations Registered modules, key - module name, value - module instance
* @param logger Logger for this script
* @param adjustContext Fn for tuning context (looks redundant)
* @param classLoader ClassLoader for loading all classes for this script
* @param exceptionTranslator Converts exceptions to error messages
* @param defaultMaxMemPages The maximum number of memory pages when a module doesn't say
* @param includeBinaryInCompiledClass Store binary wasm code to compiled class
* file as annotation [asmble.annotation.WasmModule]
*/
data class ScriptContext(
val packageName: String,
val modules: List<Module.Compiled> = emptyList(),
@ -29,8 +43,10 @@ data class ScriptContext(
ScriptContext.SimpleClassLoader(ScriptContext::class.java.classLoader, logger),
val exceptionTranslator: ExceptionTranslator = ExceptionTranslator,
val defaultMaxMemPages: Int = 1,
val includeBinaryInCompiledClass: Boolean = false
val includeBinaryInCompiledClass: Boolean = false,
val memoryBuilder: MemoryBufferBuilder? = null
) : Logger by logger {
fun withHarnessRegistered(out: PrintWriter = PrintWriter(System.out, true)) =
withModuleRegistered("spectest", Module.Native(TestHarness(out)))
@ -330,4 +346,4 @@ data class ScriptContext(
defineClass(className, bytes, 0, bytes.size)
}
}
}
}

View File

@ -3,6 +3,8 @@ package asmble.run.jvm
import asmble.annotation.WasmExport
import asmble.annotation.WasmExternalKind
import asmble.compile.jvm.Mem
import asmble.compile.jvm.MemoryBuffer
import asmble.compile.jvm.MemoryByteBuffer
import java.io.PrintWriter
import java.lang.invoke.MethodHandle
import java.nio.ByteBuffer
@ -17,10 +19,10 @@ open class TestHarness(val out: PrintWriter) {
val global_f32 = 666.6f
val global_f64 = 666.6
val table = arrayOfNulls<MethodHandle>(10)
val memory = ByteBuffer.
val memory = MemoryByteBuffer(ByteBuffer.
allocateDirect(2 * Mem.PAGE_SIZE).
order(ByteOrder.LITTLE_ENDIAN).
limit(Mem.PAGE_SIZE) as ByteBuffer
limit(Mem.PAGE_SIZE) as ByteBuffer) as MemoryBuffer
// Note, we have all of these overloads because my import method
// resolver is simple right now and only finds exact methods via

View File

@ -5,7 +5,6 @@ import asmble.ast.Node
import asmble.run.jvm.ScriptContext
import asmble.util.get
import org.junit.Test
import java.nio.ByteBuffer
import java.util.*
import kotlin.test.assertEquals
@ -35,9 +34,9 @@ class LargeDataTest : TestBase() {
val cls = ScriptContext.SimpleClassLoader(javaClass.classLoader, logger).fromBuiltContext(ctx)
// Instantiate it, get the memory out, and check it
val field = cls.getDeclaredField("memory").apply { isAccessible = true }
val buf = field[cls.newInstance()] as ByteBuffer
val buf = field[cls.newInstance()] as MemoryByteBuffer
// Grab all + 1 and check values
val bytesActual = ByteArray(70001).also { buf.get(0, it) }
val bytesActual = ByteArray(70001).also { buf.bb.get(0, it) }
bytesActual.forEachIndexed { index, byte ->
assertEquals(if (index == 70000) 0.toByte() else bytesExpected[index], byte)
}

View File

@ -4,10 +4,10 @@ import asmble.TestBase
import asmble.ast.Node
import asmble.compile.jvm.AstToAsm
import asmble.compile.jvm.ClsContext
import asmble.compile.jvm.MemoryByteBuffer
import org.junit.Assert
import org.junit.Test
import org.objectweb.asm.MethodTooLargeException
import java.nio.ByteBuffer
import java.util.*
import kotlin.test.assertEquals
@ -62,7 +62,7 @@ class LargeFuncTest : TestBase() {
// Run someFunc
cls.getMethod("someFunc").invoke(inst)
// Get the memory out
val mem = cls.getMethod("getMemory").invoke(inst) as ByteBuffer
val mem = cls.getMethod("getMemory").invoke(inst) as MemoryByteBuffer
// Read out the mem values
(0 until numInsnChunks).forEach { assertEquals(it * (it - 1), mem.getInt(it * 4)) }
}

View File

@ -0,0 +1,51 @@
package asmble.run.jvm
import asmble.TestBase
import org.junit.Test
import java.io.PrintWriter
import java.io.StringWriter
import kotlin.test.assertEquals
class LoggerModuleTest : TestBase() {
@Test
fun writeAndFlushTest() {
val stream = StringWriter()
val logger = LoggerModule(PrintWriter(stream))
logger.flush() // checks that no raise error
val testString = "test String for log to stdout"
for (byte: Byte in testString.toByteArray()) {
logger.write(byte.toInt())
}
logger.flush()
val loggedString = stream.toString()
assertEquals(testString, loggedString)
}
@Test
fun writeAndFlushMoreThanLoggerBufferTest() {
val stream = StringWriter()
// logger buffer has 65Kb size
val logger = LoggerModule(PrintWriter(stream))
val testString = longString(65_000 * 2) // twice as much as logger buffer
for (byte: Byte in testString.toByteArray()) {
logger.write(byte.toInt())
}
logger.flush()
val loggedString = stream.toString()
assertEquals(testString, loggedString)
}
private fun longString(size: Int): String {
val stringBuffer = StringBuffer()
for (idx: Int in (1 until size)) {
stringBuffer.append((idx % Byte.MAX_VALUE).toChar())
}
return stringBuffer.toString()
}
}

View File

@ -3,6 +3,8 @@ package asmble.run.jvm
import asmble.BaseTestUnit
import asmble.TestBase
import asmble.annotation.WasmModule
import asmble.compile.jvm.MemoryBufferBuilder
import asmble.compile.jvm.MemoryByteBuffer
import asmble.io.AstToBinary
import asmble.io.AstToSExpr
import asmble.io.ByteWriter
@ -12,6 +14,7 @@ import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.nio.ByteBuffer
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@ -40,7 +43,10 @@ abstract class TestRunner<out T : BaseTestUnit>(val unit: T) : TestBase() {
adjustContext = { it.copy(eagerFailLargeMemOffset = false) },
defaultMaxMemPages = unit.defaultMaxMemPages,
// Include the binary data so we can check it later
includeBinaryInCompiledClass = true
includeBinaryInCompiledClass = true,
memoryBuilder = MemoryBufferBuilder { it ->
MemoryByteBuffer(ByteBuffer.allocateDirect(it))
}
).withHarnessRegistered(PrintWriter(OutputStreamWriter(out, Charsets.UTF_8), true))
// This will fail assertions as necessary

View File

@ -9,9 +9,3 @@ Compile Rust to WASM and then to the JVM. In order of complexity:
* [rust-simple](rust-simple)
* [rust-string](rust-string)
* [rust-regex](rust-regex)
### C/C++
Compile C to WASM and then to the JVM. In order of complexity:
* [c-simple](c-simple)

View File

@ -2,8 +2,9 @@
extern crate regex;
use std::ptr::NonNull;
use regex::Regex;
use std::heap::{Alloc, Heap, Layout};
use std::alloc::{Alloc, Global, Layout};
use std::mem;
use std::str;
@ -37,17 +38,17 @@ pub extern "C" fn match_count(r: *mut Regex, str_ptr: *mut u8, len: usize) -> us
}
#[no_mangle]
pub extern "C" fn alloc(size: usize) -> *mut u8 {
pub extern "C" fn alloc(size: usize) -> NonNull<u8> {
unsafe {
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
Heap.alloc(layout).unwrap()
Global.alloc(layout).unwrap()
}
}
#[no_mangle]
pub extern "C" fn dealloc(ptr: *mut u8, size: usize) {
pub extern "C" fn dealloc(ptr: NonNull<u8>, size: usize) {
unsafe {
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
Heap.dealloc(ptr, layout);
Global.dealloc(ptr, layout);
}
}

View File

@ -3,6 +3,9 @@ package asmble.examples.rustregex;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Implementation of {@link RegexLib} based on `java.util.regex`.
*/
public class JavaLib implements RegexLib<String> {
@Override
public JavaPattern compile(String str) {

View File

@ -5,6 +5,10 @@ import asmble.generated.RustRegex;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
/**
* Implementation of {@link RegexLib} based on `asmble.generated.RustRegex` that
* was composed from Rust code (see lib.rs).
*/
public class RustLib implements RegexLib<RustLib.Ptr> {
// 600 pages is enough for our use

View File

@ -1,6 +1,7 @@
#![feature(allocator_api)]
use std::heap::{Alloc, Heap, Layout};
use std::ptr::NonNull;
use std::alloc::{Alloc, Global, Layout};
use std::ffi::{CString};
use std::mem;
use std::os::raw::c_char;
@ -30,17 +31,17 @@ pub extern "C" fn prepend_from_rust(ptr: *mut u8, len: usize) -> *const c_char {
}
#[no_mangle]
pub extern "C" fn alloc(size: usize) -> *mut u8 {
pub extern "C" fn alloc(size: usize) -> NonNull<u8> {
unsafe {
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
Heap.alloc(layout).unwrap()
Global.alloc(layout).unwrap()
}
}
#[no_mangle]
pub extern "C" fn dealloc(ptr: *mut u8, size: usize) {
pub extern "C" fn dealloc(ptr: NonNull<u8>, size: usize) {
unsafe {
let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
Heap.dealloc(ptr, layout);
Global.dealloc(ptr, layout);
}
}