Begin linker dev for issue #8

This commit is contained in:
Chad Retz 2017-06-08 11:55:03 -05:00
parent 43333edfd0
commit 3e912b2b15
20 changed files with 565 additions and 63 deletions

View File

@ -0,0 +1,11 @@
package asmble.annotation;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface WasmExport {
String value();
WasmExternalKind kind() default WasmExternalKind.FUNCTION;
}

View File

@ -0,0 +1,5 @@
package asmble.annotation;
public enum WasmExternalKind {
MEMORY, GLOBAL, FUNCTION, TABLE
}

View File

@ -0,0 +1,16 @@
package asmble.annotation;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface WasmImport {
String module();
String field();
// The JVM method descriptor of an export that will match this
String desc();
WasmExternalKind kind();
int resizableLimitInitial() default -1;
int resizableLimitMaximum() default -1;
}

View File

@ -0,0 +1,11 @@
package asmble.annotation;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WasmModule {
String name() default "";
String binary() default "";
}

View File

@ -1,10 +0,0 @@
package asmble.annotation;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface WasmName {
String value();
}

View File

@ -2,7 +2,7 @@ group 'asmble'
version '0.1.0'
buildscript {
ext.kotlin_version = '1.1.1'
ext.kotlin_version = '1.1.2'
ext.asm_version = '5.2'
repositories {

View File

@ -31,6 +31,17 @@ open class Compile : Command<Compile.Args>() {
opt = "out",
desc = "The file name to output to. Can be '--' to write to stdout.",
default = "<outClass.class>"
),
name = bld.arg(
name = "name",
opt = "name",
desc = "The name to use for this module. Will override the name on the module if present.",
default = "<name on module or none>"
).takeIf { it != "<name on module or none>" },
includeBinary = bld.flag(
opt = "bindata",
desc = "Embed the WASM binary as an annotation on the class.",
lowPriority = true
)
).also { bld.done() }
@ -40,7 +51,7 @@ open class Compile : Command<Compile.Args>() {
if (args.inFormat != "<use file extension>") args.inFormat
else args.inFile.substringAfterLast('.', "<unknown>")
val script = Translate.inToAst(args.inFile, inFormat)
val mod = (script.commands.firstOrNull() as? Script.Cmd.Module)?.module ?:
val mod = (script.commands.firstOrNull() as? Script.Cmd.Module) ?:
error("Only a single sexpr for (module) allowed")
val outStream = when (args.outFile) {
"<outClass.class>" -> FileOutputStream(args.outClass.substringAfterLast('.') + ".class")
@ -51,8 +62,10 @@ open class Compile : Command<Compile.Args>() {
val ctx = ClsContext(
packageName = if (!args.outClass.contains('.')) "" else args.outClass.substringBeforeLast('.'),
className = args.outClass.substringAfterLast('.'),
mod = mod,
logger = logger
mod = mod.module,
modName = args.name ?: mod.name,
logger = logger,
includeBinary = args.includeBinary
)
AstToAsm.fromModule(ctx)
outStream.write(ctx.cls.withComputedFramesAndMaxs())
@ -63,7 +76,9 @@ open class Compile : Command<Compile.Args>() {
val inFile: String,
val inFormat: String,
val outClass: String,
val outFile: String
val outFile: String,
val name: String?,
val includeBinary: Boolean
)
companion object : Compile()

View File

@ -0,0 +1,67 @@
package asmble.cli
import asmble.compile.jvm.Linker
import asmble.compile.jvm.withComputedFramesAndMaxs
import java.io.FileOutputStream
open class Link : Command<Link.Args>() {
override val name = "link"
override val desc = "Link WebAssembly modules in a single class file. TODO: not done"
override fun args(bld: Command.ArgsBuilder) = Args(
outFile = bld.arg(
name = "outFile",
opt = "out",
desc = "The file name to output to. Can be '--' to write to stdout.",
default = "<outClass.class>"
),
modules = bld.args(
name = "modules",
desc = "The fully qualified class name of the modules on the classpath to link. A module name can be" +
" added after an equals sign to set/override the existing module name."
),
outClass = bld.arg(
name = "outClass",
desc = "The fully qualified class name."
),
defaultMaxMem = bld.arg(
name = "defaultMaxMem",
opt = "maxmem",
desc = "The max number of pages to build memory with when not specified by the module/import.",
default = "10"
).toInt()
).also { bld.done() }
override fun run(args: Args) {
val outStream = when (args.outFile) {
"<outClass.class>" -> FileOutputStream(args.outClass.substringAfterLast('.') + ".class")
"--" -> System.out
else -> FileOutputStream(args.outFile)
}
outStream.use { outStream ->
val ctx = Linker.Context(
classes = args.modules.map { module ->
val pieces = module.split('=', limit = 2)
Linker.ModuleClass(
cls = Class.forName(pieces.first()),
overrideName = pieces.getOrNull(1)
)
},
className = args.outClass,
defaultMaxMemPages = args.defaultMaxMem
)
Linker.link(ctx)
outStream.write(ctx.cls.withComputedFramesAndMaxs())
}
}
data class Args(
val modules: List<String>,
val outClass: String,
val outFile: String,
val defaultMaxMem: Int
)
companion object : Link()
}

View File

@ -3,7 +3,7 @@ package asmble.cli
import asmble.util.Logger
import kotlin.system.exitProcess
val commands = listOf(Compile, Help, Invoke, Run, Translate)
val commands = listOf(Compile, Help, Invoke, Link, Run, Translate)
fun main(args: Array<String>) {
if (args.isEmpty()) return println(

View File

@ -9,6 +9,9 @@ import org.objectweb.asm.tree.*
import org.objectweb.asm.util.TraceClassVisitor
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.reflect.Constructor
import java.lang.reflect.Executable
import java.lang.reflect.Method
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KProperty
@ -60,6 +63,13 @@ val Class<*>.valueType: Node.Type.Value? get() = when (this) {
else -> error("Unrecognized value type class: $this")
}
val Executable.ref: TypeRef get() = when (this) {
is Method -> TypeRef(Type.getType(this))
is Constructor<*> -> TypeRef(Type.getType(this))
else -> error("Unknown executable $this")
}
val KProperty<*>.declarer: Class<*> get() = this.javaField!!.declaringClass
val KProperty<*>.asmDesc: String get() = Type.getDescriptor(this.javaField!!.type)
@ -178,11 +188,12 @@ fun MethodNode.toAsmString(): String {
val Node.Type.Func.asmDesc: String get() =
(this.ret?.typeRef ?: Void::class.ref).asMethodRetDesc(*this.params.map { it.typeRef }.toTypedArray())
fun ClassNode.withComputedFramesAndMaxs(): ByteArray {
fun ClassNode.withComputedFramesAndMaxs(
cw: ClassWriter = ClassWriter(ClassWriter.COMPUTE_FRAMES + ClassWriter.COMPUTE_MAXS)
): ByteArray {
// Note, compute maxs adds a bunch of NOPs for unreachable code.
// See $func12 of block.wast. I don't believe the extra time over the
// instructions to remove the NOPs is worth it.
val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES + ClassWriter.COMPUTE_MAXS)
this.accept(cw)
return cw.toByteArray()
}

View File

@ -1,10 +1,17 @@
package asmble.compile.jvm
import asmble.annotation.WasmExport
import asmble.annotation.WasmExternalKind
import asmble.annotation.WasmImport
import asmble.annotation.WasmModule
import asmble.ast.Node
import asmble.io.AstToBinary
import asmble.io.ByteWriter
import asmble.util.Either
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.tree.*
import java.io.ByteArrayOutputStream
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
@ -13,12 +20,13 @@ open class AstToAsm {
fun fromModule(ctx: ClsContext) {
// Invoke dynamic among other things
ctx.cls.superName = Object::class.ref.asmName
ctx.cls.version = Opcodes.V1_7
ctx.cls.version = Opcodes.V1_8
ctx.cls.access += Opcodes.ACC_PUBLIC
addFields(ctx)
addConstructors(ctx)
addFuncs(ctx)
addExports(ctx)
addAnnotations(ctx)
}
fun addFields(ctx: ClsContext) {
@ -85,7 +93,7 @@ open class AstToAsm {
func = initializeConstructorTables(ctx, func, 0)
func = executeConstructorStartFunction(ctx, func, 0)
func = func.addInsns(InsnNode(Opcodes.RETURN))
ctx.cls.methods.add(func.toMethodNode())
ctx.cls.methods.add(toConstructorNode(ctx, func))
}
fun addMaxMemConstructor(ctx: ClsContext) {
@ -107,7 +115,7 @@ open class AstToAsm {
MethodInsnNode(Opcodes.INVOKESPECIAL, ctx.thisRef.asmName, "<init>", desc, false),
InsnNode(Opcodes.RETURN)
)
ctx.cls.methods.add(func.toMethodNode())
ctx.cls.methods.add(toConstructorNode(ctx, func))
}
fun addMemClassConstructor(ctx: ClsContext) {
@ -148,7 +156,7 @@ open class AstToAsm {
func = executeConstructorStartFunction(ctx, func, 1)
func = func.addInsns(InsnNode(Opcodes.RETURN))
ctx.cls.methods.add(func.toMethodNode())
ctx.cls.methods.add(toConstructorNode(ctx, func))
}
fun addMemDefaultConstructor(ctx: ClsContext) {
@ -167,7 +175,7 @@ open class AstToAsm {
MethodInsnNode(Opcodes.INVOKESPECIAL, ctx.thisRef.asmName, "<init>", desc, false),
InsnNode(Opcodes.RETURN)
)
ctx.cls.methods.add(func.toMethodNode())
ctx.cls.methods.add(toConstructorNode(ctx, func))
}
fun constructorImportTypes(ctx: ClsContext) =
@ -176,6 +184,61 @@ open class AstToAsm {
ctx.importGlobals.map { MethodHandle::class.ref } +
ctx.mod.imports.filter { it.kind is Node.Import.Kind.Table }.map { Array<MethodHandle>::class.ref }
fun toConstructorNode(ctx: ClsContext, func: Func) = mutableListOf<List<AnnotationNode>>().let { paramAnns ->
// If the first param is a mem class and imported, add annotation
// Otherwise if it is a mem class and not-imported or an int, no annotations
// Otherwise do nothing because the rest of the params are imports
func.params.firstOrNull()?.also { firstParam ->
if (firstParam == Int::class.ref) {
paramAnns.add(emptyList())
} else if (firstParam == ctx.mem.memType) {
val importMem = ctx.mod.imports.find { it.kind is Node.Import.Kind.Memory }
if (importMem == null) paramAnns.add(emptyList())
else paramAnns.add(listOf(importAnnotation(ctx, importMem)))
}
}
// All non-mem imports one after another
ctx.importFuncs.forEach { paramAnns.add(listOf(importAnnotation(ctx, it))) }
ctx.importGlobals.forEach { paramAnns.add(listOf(importAnnotation(ctx, it))) }
ctx.mod.imports.forEach {
if (it.kind is Node.Import.Kind.Table) paramAnns.add(listOf(importAnnotation(ctx, it)))
}
func.toMethodNode().also { it.visibleParameterAnnotations = paramAnns.toTypedArray() }
}
fun importAnnotation(ctx: ClsContext, import: Node.Import) = AnnotationNode(WasmImport::class.ref.asmDesc).also {
it.values = mutableListOf<Any>("module", import.module, "field", import.field)
fun addValues(desc: String, limits: Node.ResizableLimits? = null) {
it.values.add("desc")
it.values.add(desc)
if (limits != null) {
it.values.add("resizableLimitInitial")
it.values.add(limits.initial)
if (limits.maximum != null) {
it.values.add("resizableLimitMaximum")
it.values.add(limits.maximum)
}
}
it.values.add("kind")
it.values.add(arrayOf(WasmExternalKind::class.ref.asmDesc, when (import.kind) {
is Node.Import.Kind.Func -> WasmExternalKind.FUNCTION.name
is Node.Import.Kind.Table -> WasmExternalKind.TABLE.name
is Node.Import.Kind.Memory -> WasmExternalKind.MEMORY.name
is Node.Import.Kind.Global -> WasmExternalKind.GLOBAL.name
}))
}
when (import.kind) {
is Node.Import.Kind.Func ->
ctx.typeAtIndex(import.kind.typeIndex).let { addValues(it.asmDesc) }
is Node.Import.Kind.Table ->
addValues(Array<MethodHandle>::class.ref.asMethodRetDesc(), import.kind.type.limits)
is Node.Import.Kind.Memory ->
addValues(ctx.mem.memType.asMethodRetDesc(), import.kind.type.limits)
is Node.Import.Kind.Global ->
addValues(import.kind.type.contentType.typeRef.asMethodRetDesc())
}
}
fun setConstructorGlobalImports(ctx: ClsContext, func: Func, paramsBeforeImports: Int) =
ctx.importGlobals.indices.fold(func) { func, importIndex ->
func.addInsns(
@ -411,6 +474,13 @@ open class AstToAsm {
}
}
fun exportAnnotation(export: Node.Export) = AnnotationNode(WasmExport::class.ref.asmDesc).also {
it.values = listOf(
"value", export.field,
"kind", arrayOf(WasmExternalKind::class.ref.asmDesc, export.kind.name)
)
}
fun addExportFunc(ctx: ClsContext, export: Node.Export) {
val funcType = ctx.funcTypeAtIndex(export.index)
val method = MethodNode(Opcodes.ACC_PUBLIC, export.field.javaIdent, funcType.asmDesc, null, null)
@ -452,6 +522,7 @@ open class AstToAsm {
Node.Type.Value.F32 -> Opcodes.FRETURN
Node.Type.Value.F64 -> Opcodes.DRETURN
}))
method.visibleAnnotations = listOf(exportAnnotation(export))
ctx.cls.methods.plusAssign(method)
}
@ -481,6 +552,7 @@ open class AstToAsm {
Node.Type.Value.F32 -> Opcodes.FRETURN
Node.Type.Value.F64 -> Opcodes.DRETURN
}))
method.visibleAnnotations = listOf(exportAnnotation(export))
ctx.cls.methods.plusAssign(method)
}
@ -494,6 +566,7 @@ open class AstToAsm {
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName, "memory", ctx.mem.memType.asmDesc),
InsnNode(Opcodes.ARETURN)
)
method.visibleAnnotations = listOf(exportAnnotation(export))
ctx.cls.methods.plusAssign(method)
}
@ -507,6 +580,7 @@ open class AstToAsm {
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName, "table", Array<MethodHandle>::class.ref.asmDesc),
InsnNode(Opcodes.ARETURN)
)
method.visibleAnnotations = listOf(exportAnnotation(export))
ctx.cls.methods.plusAssign(method)
}
@ -516,5 +590,22 @@ open class AstToAsm {
})
}
fun addAnnotations(ctx: ClsContext) {
val annotationVals = mutableListOf<Any>()
ctx.modName?.let { annotationVals.addAll(listOf("name", it)) }
if (ctx.includeBinary) {
// We are going to store this as a string of bytes in an annotation on the class. The linker
// used to use this, but no longer does so it is opt-in for others to use. We choose to use an
// annotation instead of an attribute for the same reasons Scala chose to make the switch in
// 2.8+: Easier runtime reflection despite some size cost.
annotationVals.addAll(listOf("binary", ByteArrayOutputStream().also {
ByteWriter.OutputStream(it).also { AstToBinary.fromModule(it, ctx.mod) }
}.toByteArray().toString(Charsets.ISO_8859_1)))
}
ctx.cls.visibleAnnotations = listOf(
AnnotationNode(WasmModule::class.ref.asmDesc).also { it.values = annotationVals }
)
}
companion object : AstToAsm()
}

View File

@ -16,6 +16,7 @@ data class ClsContext(
val mod: Node.Module,
val cls: ClassNode = ClassNode().also { it.name = (packageName.replace('.', '/') + "/$className").trimStart('/') },
val mem: Mem = ByteBufferMem,
val modName: String? = null,
val reworker: InsnReworker = InsnReworker,
val logger: Logger = Logger.Print(Logger.Level.OFF),
val funcBuilder: FuncBuilder = FuncBuilder,
@ -26,7 +27,8 @@ data class ClsContext(
val preventMemIndexOverflow: Boolean = false,
val accurateNanBits: Boolean = true,
val checkSignedDivIntegerOverflow: Boolean = true,
val jumpTableChunkSize: Int = 5000
val jumpTableChunkSize: Int = 5000,
val includeBinary: Boolean = false
) : Logger by logger {
val importFuncs: List<Node.Import> by lazy { mod.imports.filter { it.kind is Node.Import.Kind.Func } }
val importGlobals: List<Node.Import> by lazy { mod.imports.filter { it.kind is Node.Import.Kind.Global } }

View File

@ -0,0 +1,244 @@
package asmble.compile.jvm
import asmble.annotation.WasmExport
import asmble.annotation.WasmExternalKind
import asmble.annotation.WasmImport
import asmble.annotation.WasmModule
import org.objectweb.asm.Handle
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.tree.*
import java.lang.invoke.MethodHandle
open class Linker {
fun link(ctx: Context) {
// Quick check to prevent duplicate names
ctx.classes.groupBy { it.name }.values.forEach {
require(it.size == 1) { "Duplicate module name: ${it.first().name}"}
}
// Common items
ctx.cls.superName = Object::class.ref.asmName
ctx.cls.version = Opcodes.V1_8
ctx.cls.access += Opcodes.ACC_PUBLIC
addConstructor(ctx)
addDefaultMaxMemField(ctx)
// Go over each module and add its creation and instance methods
ctx.classes.forEach {
addCreationMethod(ctx, it)
addInstanceField(ctx, it)
addInstanceMethod(ctx, it)
}
TODO()
}
fun addConstructor(ctx: Context) {
// Just the default empty constructor
ctx.cls.methods.plusAssign(
Func(
access = Opcodes.ACC_PUBLIC,
name = "<init>",
params = emptyList(),
ret = Void::class.ref,
insns = listOf(
VarInsnNode(Opcodes.ALOAD, 0),
MethodInsnNode(Opcodes.INVOKESPECIAL, Object::class.ref.asmName, "<init>", "()V", false),
InsnNode(Opcodes.RETURN)
)
).toMethodNode()
)
}
fun addDefaultMaxMemField(ctx: Context) {
(Int.MAX_VALUE / Mem.PAGE_SIZE).let { maxAllowed ->
require(ctx.defaultMaxMemPages <= maxAllowed) {
"Page size ${ctx.defaultMaxMemPages} over max allowed $maxAllowed"
}
}
ctx.cls.fields.plusAssign(FieldNode(
// Make it volatile since it will be publicly mutable
Opcodes.ACC_PUBLIC + Opcodes.ACC_VOLATILE,
"defaultMaxMem",
"I",
null,
ctx.defaultMaxMemPages * Mem.PAGE_SIZE
))
}
fun addCreationMethod(ctx: Context, mod: ModuleClass) {
// The creation method accepts everything needed for import in order of
// imports. For creating a mod w/ self-built memory, we use a default max
// mem field on the linkage class if there isn't a default already.
val params = mod.importClasses(ctx)
var func = Func(
access = Opcodes.ACC_PROTECTED,
name = "create" + mod.name.javaIdent.capitalize(),
params = params.map(ModuleClass::ref),
ret = mod.ref
)
// The stack here on our is for building params to constructor...
// The constructor we'll use is:
// * Mem-class based constructor if it's an import
// * Max-mem int based constructor if mem is self-built and doesn't have a no-mem-no-max ctr
// * Should be only single constructor with imports when there's no mem
val memClassCtr = mod.cls.constructors.find { it.parameters.firstOrNull()?.type?.ref == ctx.mem.memType }
val constructor = if (memClassCtr == null) mod.cls.constructors.singleOrNull() else {
// Use the import annotated one if there
if (memClassCtr.parameters.first().isAnnotationPresent(WasmImport::class.java)) memClassCtr else {
// If there is a non-int-starting constructor, we want to use that
val nonMaxMemCtr = mod.cls.constructors.find {
it != memClassCtr && it.parameters.firstOrNull()?.type != Integer.TYPE
}
if (nonMaxMemCtr != null) nonMaxMemCtr else {
// Use the max-mem constructor and put the int on the stack
func = func.addInsns(
VarInsnNode(Opcodes.ALOAD, 0),
FieldInsnNode(Opcodes.GETFIELD, ctx.cls.name, "defaultMaxMem", "I")
)
mod.cls.constructors.find { it.parameters.firstOrNull()?.type != Integer.TYPE }
}
}
}
if (constructor == null) error("Unable to find suitable constructor for ${mod.cls}")
// Now just go over the imports and put them on the stack
func = constructor.parameters.fold(func) { func, param ->
param.getAnnotation(WasmImport::class.java).let { import ->
when (import.kind) {
// Invoke the mem handle to get the mem
// TODO: for imported memory, fail if import.limit < limits.init * page size at runtime
// TODO: for imported memory, fail if import.cap > limits.max * page size at runtime
WasmExternalKind.MEMORY -> func.addInsns(
VarInsnNode(Opcodes.ALOAD, 1 + params.indexOfFirst { it.name == import.module }),
ctx.resolveImportHandle(import).let { memGet ->
MethodInsnNode(Opcodes.INVOKEVIRTUAL, memGet.owner, memGet.name, memGet.desc, false)
}
)
// Bind the method
WasmExternalKind.FUNCTION -> func.addInsns(
LdcInsnNode(ctx.resolveImportHandle(import)),
VarInsnNode(Opcodes.ALOAD, 1 + params.indexOfFirst { it.name == import.module }),
MethodHandle::bindTo.invokeVirtual()
)
// Bind the getter
WasmExternalKind.GLOBAL -> func.addInsns(
LdcInsnNode(ctx.resolveImportHandle(import)),
VarInsnNode(Opcodes.ALOAD, 1 + params.indexOfFirst { it.name == import.module }),
MethodHandle::bindTo.invokeVirtual()
)
// Invoke to get handle array
// TODO: for imported table, fail if import.size < limits.init * page size at runtime
// TODO: for imported table, fail if import.size > limits.max * page size at runtime
WasmExternalKind.TABLE -> func.addInsns(
VarInsnNode(Opcodes.ALOAD, 1 + params.indexOfFirst { it.name == import.module }),
ctx.resolveImportHandle(import).let { tblGet ->
MethodInsnNode(Opcodes.INVOKEVIRTUAL, tblGet.owner, tblGet.name, tblGet.desc, false)
}
)
}
}
}
// Now with all items on the stack we can instantiate and return
func = func.addInsns(
TypeInsnNode(Opcodes.NEW, mod.ref.asmName),
InsnNode(Opcodes.DUP),
MethodInsnNode(
Opcodes.INVOKESPECIAL,
mod.ref.asmName,
"<init>",
constructor.ref.asmDesc,
false
),
InsnNode(Opcodes.ARETURN)
)
ctx.cls.methods.plusAssign(func.toMethodNode())
}
fun addInstanceField(ctx: Context, mod: ModuleClass) {
// Simple protected field that is lazily populated (but doesn't need to be volatile)
ctx.cls.fields.plusAssign(
FieldNode(Opcodes.ACC_PROTECTED, "instance" + mod.name.javaIdent.capitalize(),
mod.ref.asmDesc, null, null)
)
}
fun addInstanceMethod(ctx: Context, mod: ModuleClass) {
// The instance method accepts no parameters. It lazily populates a field by calling the
// creation method. The parameters for the creation method are the imports that are
// accessed via their instance methods. The entire method is synchronized as that is the
// most straightforward way to thread-safely lock the lazy population for now.
val params = mod.importClasses(ctx)
var func = Func(
access = Opcodes.ACC_PUBLIC + Opcodes.ACC_SYNCHRONIZED,
name = mod.name.javaIdent,
ret = mod.ref
)
val alreadyThereLabel = LabelNode()
func = func.addInsns(
VarInsnNode(Opcodes.ALOAD, 0),
FieldInsnNode(Opcodes.GETFIELD, ctx.cls.name,
"instance" + mod.name.javaIdent.capitalize(), mod.ref.asmDesc),
JumpInsnNode(Opcodes.IFNONNULL, alreadyThereLabel),
VarInsnNode(Opcodes.ALOAD, 0)
)
func = params.fold(func) { func, importMod ->
func.addInsns(
VarInsnNode(Opcodes.ALOAD, 0),
MethodInsnNode(Opcodes.INVOKEVIRTUAL, importMod.ref.asmName,
importMod.name.javaIdent, importMod.ref.asMethodRetDesc(), false)
)
}
func = func.addInsns(
FieldInsnNode(Opcodes.PUTFIELD, ctx.cls.name,
"instance" + mod.name.javaIdent.capitalize(), mod.ref.asmDesc),
alreadyThereLabel,
VarInsnNode(Opcodes.ALOAD, 0),
FieldInsnNode(Opcodes.GETFIELD, ctx.cls.name,
"instance" + mod.name.javaIdent.capitalize(), mod.ref.asmDesc),
InsnNode(Opcodes.ARETURN)
)
ctx.cls.methods.plusAssign(func)
}
class ModuleClass(val cls: Class<*>, overrideName: String? = null) {
val name = overrideName ?:
cls.getDeclaredAnnotation(WasmModule::class.java)?.name ?: error("No module name available for class $cls")
val ref = TypeRef(Type.getType(cls))
fun importClasses(ctx: Context): List<ModuleClass> {
// Try to find constructor with mem class first, otherwise there should be only one
val constructorWithImports = cls.constructors.find {
it.parameters.firstOrNull()?.type?.ref == ctx.mem.memType
} ?: cls.constructors.singleOrNull() ?: error("Unable to find suitable constructor for $cls")
return constructorWithImports.parameters.toList().mapNotNull {
it.getAnnotation(WasmImport::class.java)?.module
}.distinct().map(ctx::namedModuleClass)
}
}
data class Context(
val classes: List<ModuleClass>,
val className: String,
val cls: ClassNode = ClassNode().also { it.name = className.replace('.', '/') },
val mem: Mem = ByteBufferMem,
val defaultMaxMemPages: Int = 10
) {
fun namedModuleClass(name: String) = classes.find { it.name == name } ?: error("No module named '$name'")
fun resolveImportMethod(import: WasmImport) =
namedModuleClass(import.module).cls.methods.find { method ->
method.getAnnotation(WasmExport::class.java)?.value == import.field &&
method.ref.asmDesc == import.desc
} ?: error("Unable to find export named '${import.field}' in module '${import.module}'")
fun resolveImportHandle(import: WasmImport) = resolveImportMethod(import).let { method ->
Handle(Opcodes.INVOKEVIRTUAL, method.declaringClass.ref.asmName, method.name, method.ref.asmDesc, false)
}
}
companion object : Linker()
}

View File

@ -1,6 +1,7 @@
package asmble.run.jvm
import asmble.annotation.WasmName
import asmble.annotation.WasmExport
import asmble.annotation.WasmExternalKind
import asmble.ast.Node
import asmble.compile.jvm.Mem
import asmble.compile.jvm.ref
@ -8,13 +9,25 @@ import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
import java.lang.reflect.Constructor
import java.lang.reflect.Modifier
interface Module {
fun bindMethod(ctx: ScriptContext, wasmName: String, javaName: String, type: MethodType): MethodHandle?
fun bindMethod(
ctx: ScriptContext,
wasmName: String,
wasmKind: WasmExternalKind,
javaName: String,
type: MethodType
): MethodHandle?
data class Composite(val modules: List<Module>) : Module {
override fun bindMethod(ctx: ScriptContext, wasmName: String, javaName: String, type: MethodType) =
modules.asSequence().mapNotNull { it.bindMethod(ctx, wasmName, javaName, type) }.singleOrNull()
override fun bindMethod(
ctx: ScriptContext,
wasmName: String,
wasmKind: WasmExternalKind,
javaName: String,
type: MethodType
) = modules.asSequence().mapNotNull { it.bindMethod(ctx, wasmName, wasmKind, javaName, type) }.singleOrNull()
}
interface Instance : Module {
@ -22,16 +35,22 @@ interface Module {
// Guaranteed to be the same instance when there is no error
fun instance(ctx: ScriptContext): Any
override fun bindMethod(ctx: ScriptContext, wasmName: String, javaName: String, type: MethodType) =
try {
MethodHandles.lookup().bind(instance(ctx), javaName, type)
} catch (_: NoSuchMethodException) {
// Try any method w/ the proper annotation
cls.methods.mapNotNull { method ->
if (method.getAnnotation(WasmName::class.java)?.value != wasmName) null
else MethodHandles.lookup().unreflect(method).bindTo(instance(ctx)).takeIf { it.type() == type }
}.singleOrNull()
override fun bindMethod(
ctx: ScriptContext,
wasmName: String,
wasmKind: WasmExternalKind,
javaName: String,
type: MethodType
) = cls.methods.filter {
// @WasmExport match or just javaName match
Modifier.isPublic(it.modifiers) &&
!Modifier.isStatic(it.modifiers) &&
it.getDeclaredAnnotation(WasmExport::class.java).let { ann ->
if (ann == null) it.name == javaName else ann.value == wasmName && ann.kind == wasmKind
}
}.mapNotNull {
MethodHandles.lookup().unreflect(it).bindTo(instance(ctx)).takeIf { it.type() == type }
}.singleOrNull()
}
data class Native(override val cls: Class<*>, val inst: Any) : Instance {

View File

@ -1,10 +1,10 @@
package asmble.run.jvm
import asmble.annotation.WasmExternalKind
import asmble.ast.Node
import asmble.ast.Script
import asmble.compile.jvm.*
import asmble.io.AstToSExpr
import asmble.io.Emscripten
import asmble.io.SExprToStr
import asmble.util.Logger
import asmble.util.toRawIntBits
@ -12,8 +12,6 @@ import asmble.util.toRawLongBits
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.Opcodes
import run.jvm.emscripten.Env
import java.io.OutputStream
import java.io.PrintWriter
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
@ -30,7 +28,8 @@ data class ScriptContext(
val classLoader: SimpleClassLoader =
ScriptContext.SimpleClassLoader(ScriptContext::class.java.classLoader, logger),
val exceptionTranslator: ExceptionTranslator = ExceptionTranslator,
val defaultMaxMemPages: Int = 1
val defaultMaxMemPages: Int = 1,
val includeBinaryInCompiledClass: Boolean = false
) : Logger by logger {
fun withHarnessRegistered(out: PrintWriter = PrintWriter(System.out, true)) =
withModuleRegistered("spectest", Module.Native(TestHarness(out)))
@ -252,7 +251,8 @@ data class ScriptContext(
packageName = packageName,
className = className,
mod = mod,
logger = logger
logger = logger,
includeBinary = includeBinaryInCompiledClass
).let(adjustContext)
AstToAsm.fromModule(ctx)
return Module.Compiled(mod, classLoader.fromBuiltContext(ctx), name, ctx.mem)
@ -262,7 +262,13 @@ data class ScriptContext(
// Find a method that matches our expectations
val module = registrations[import.module] ?: error("Unable to find module ${import.module}")
val javaName = if (getter) "get" + import.field.javaIdent.capitalize() else import.field.javaIdent
return module.bindMethod(this, import.field, javaName, methodType) ?:
val kind = when (import.kind) {
is Node.Import.Kind.Func -> WasmExternalKind.FUNCTION
is Node.Import.Kind.Table -> WasmExternalKind.TABLE
is Node.Import.Kind.Memory -> WasmExternalKind.MEMORY
is Node.Import.Kind.Global -> WasmExternalKind.GLOBAL
}
return module.bindMethod(this, import.field, kind, javaName, methodType) ?:
throw NoSuchMethodException("Cannot find import for ${import.module}::${import.field}")
}

View File

@ -1,6 +1,7 @@
package asmble.run.jvm
import asmble.annotation.WasmName
import asmble.annotation.WasmExport
import asmble.annotation.WasmExternalKind
import asmble.compile.jvm.Mem
import java.io.PrintWriter
import java.lang.invoke.MethodHandle
@ -11,10 +12,10 @@ open class TestHarness(val out: PrintWriter) {
// WASM is evil, not me:
// https://github.com/WebAssembly/spec/blob/6a01dab6d29b7c2b5dfd3bb3879bbd6ab76fd5dc/interpreter/host/import/spectest.ml#L12
@get:WasmName("global") val globalInt = 666
@get:WasmName("global") val globalLong = 666L
@get:WasmName("global") val globalFloat = 666.6f
@get:WasmName("global") val globalDouble = 666.6
@get:WasmExport("global", kind = WasmExternalKind.GLOBAL) val globalInt = 666
@get:WasmExport("global", kind = WasmExternalKind.GLOBAL) val globalLong = 666L
@get:WasmExport("global", kind = WasmExternalKind.GLOBAL) val globalFloat = 666.6f
@get:WasmExport("global", kind = WasmExternalKind.GLOBAL) val globalDouble = 666.6
val table = arrayOfNulls<MethodHandle>(10)
val memory = ByteBuffer.
allocateDirect(2 * Mem.PAGE_SIZE).

View File

@ -21,7 +21,7 @@ class SpecTestUnit(name: String, wast: String, expectedOutput: String?) : BaseTe
override fun warningInsteadOfErrReason(t: Throwable) = when (name) {
// NaN bit patterns can be off
"float_literals", "float_exprs" ->
"float_literals", "float_exprs", "float_misc" ->
if (isNanMismatch(t)) "NaN JVM bit patterns can be off" else null
// We don't hold table capacity right now
// TODO: Figure out how we want to store/retrieve table capacity. Right now

View File

@ -2,7 +2,10 @@ package asmble.run.jvm
import asmble.BaseTestUnit
import asmble.TestBase
import asmble.annotation.WasmModule
import asmble.io.AstToBinary
import asmble.io.AstToSExpr
import asmble.io.ByteWriter
import asmble.io.SExprToStr
import org.junit.Assume
import org.junit.Test
@ -36,7 +39,9 @@ abstract class TestRunner<out T : BaseTestUnit>(val unit: T) : TestBase() {
packageName = unit.packageName,
logger = this,
adjustContext = { it.copy(eagerFailLargeMemOffset = false) },
defaultMaxMemPages = unit.defaultMaxMemPages
defaultMaxMemPages = unit.defaultMaxMemPages,
// Include the binary data so we can check it later
includeBinaryInCompiledClass = true
).withHarnessRegistered(PrintWriter(OutputStreamWriter(out, Charsets.UTF_8), true))
// If there's a staticBump, we are an emscripten mod and we need to include the env
@ -80,5 +85,15 @@ abstract class TestRunner<out T : BaseTestUnit>(val unit: T) : TestBase() {
// Sadly, sometimes the expected output is trimmed in Emscripten tests
assertEquals(it.trimEnd(), out.toByteArray().toString(Charsets.UTF_8).trimEnd())
}
// Also check the annotations
scriptContext.modules.forEach { mod ->
val expectedBinaryString = ByteArrayOutputStream().also {
ByteWriter.OutputStream(it).also { AstToBinary.fromModule(it, mod.mod) }
}.toByteArray().toString(Charsets.ISO_8859_1)
val actualBinaryString =
mod.cls.getDeclaredAnnotation(WasmModule::class.java)?.binary ?: error("No annotation")
assertEquals(expectedBinaryString, actualBinaryString)
}
}
}

View File

@ -1,6 +1,6 @@
package run.jvm.emscripten;
import asmble.annotation.WasmName;
import asmble.annotation.WasmExport;
public class Common {
private final Env env;
@ -13,7 +13,7 @@ public class Common {
throw new UnsupportedOperationException();
}
@WasmName("__assert_fail")
@WasmExport("__assert_fail")
public void assertFail(int conditionPtr, int filenamePtr, int line, int funcPtr) {
throw new AssertionError("Assertion failed: " + env.mem.getCString(conditionPtr) + ", at " +
env.mem.getCString(filenamePtr) + ":" + line + ", func " + env.mem.getCString(funcPtr));
@ -29,12 +29,12 @@ public class Common {
return 0;
}
@WasmName("__cxa_call_unexpected")
@WasmExport("__cxa_call_unexpected")
public void callUnexpected(int ex) {
throw new EmscriptenException("Unexpected: " + ex);
}
@WasmName("__lock")
@WasmExport("__lock")
public void lock(int arg) {
throw new UnsupportedOperationException();
}
@ -43,7 +43,7 @@ public class Common {
throw new UnsupportedOperationException();
}
@WasmName("__unlock")
@WasmExport("__unlock")
public void unlock(int arg) {
throw new UnsupportedOperationException();
}

View File

@ -1,9 +1,7 @@
package run.jvm.emscripten;
import asmble.annotation.WasmName;
import asmble.annotation.WasmExport;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@ -23,12 +21,12 @@ public class Syscall {
this.env = env;
}
@WasmName("__syscall6")
@WasmExport("__syscall6")
public int close(int arg0, int arg1) {
throw new UnsupportedOperationException();
}
@WasmName("__syscall54")
@WasmExport("__syscall54")
public int ioctl(int which, int varargs) {
FStream fd = fd(env.getMemory().getInt(varargs));
IoctlOp op = IoctlOp.byNumber.get(env.getMemory().getInt(varargs + 4));
@ -52,12 +50,12 @@ public class Syscall {
}
}
@WasmName("__syscall140")
@WasmExport("__syscall140")
public int llseek(int arg0, int arg1) {
throw new UnsupportedOperationException();
}
@WasmName("__syscall146")
@WasmExport("__syscall146")
public int writev(int which, int varargs) {
FStream fd = fd(env.getMemory().getInt(varargs));
int iov = env.getMemory().getInt(varargs + 4);