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' version '0.1.0'
buildscript { buildscript {
ext.kotlin_version = '1.1.1' ext.kotlin_version = '1.1.2'
ext.asm_version = '5.2' ext.asm_version = '5.2'
repositories { repositories {

View File

@ -31,6 +31,17 @@ open class Compile : Command<Compile.Args>() {
opt = "out", opt = "out",
desc = "The file name to output to. Can be '--' to write to stdout.", desc = "The file name to output to. Can be '--' to write to stdout.",
default = "<outClass.class>" 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() } ).also { bld.done() }
@ -40,7 +51,7 @@ open class Compile : Command<Compile.Args>() {
if (args.inFormat != "<use file extension>") args.inFormat if (args.inFormat != "<use file extension>") args.inFormat
else args.inFile.substringAfterLast('.', "<unknown>") else args.inFile.substringAfterLast('.', "<unknown>")
val script = Translate.inToAst(args.inFile, inFormat) 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") error("Only a single sexpr for (module) allowed")
val outStream = when (args.outFile) { val outStream = when (args.outFile) {
"<outClass.class>" -> FileOutputStream(args.outClass.substringAfterLast('.') + ".class") "<outClass.class>" -> FileOutputStream(args.outClass.substringAfterLast('.') + ".class")
@ -51,8 +62,10 @@ open class Compile : Command<Compile.Args>() {
val ctx = ClsContext( val ctx = ClsContext(
packageName = if (!args.outClass.contains('.')) "" else args.outClass.substringBeforeLast('.'), packageName = if (!args.outClass.contains('.')) "" else args.outClass.substringBeforeLast('.'),
className = args.outClass.substringAfterLast('.'), className = args.outClass.substringAfterLast('.'),
mod = mod, mod = mod.module,
logger = logger modName = args.name ?: mod.name,
logger = logger,
includeBinary = args.includeBinary
) )
AstToAsm.fromModule(ctx) AstToAsm.fromModule(ctx)
outStream.write(ctx.cls.withComputedFramesAndMaxs()) outStream.write(ctx.cls.withComputedFramesAndMaxs())
@ -63,7 +76,9 @@ open class Compile : Command<Compile.Args>() {
val inFile: String, val inFile: String,
val inFormat: String, val inFormat: String,
val outClass: String, val outClass: String,
val outFile: String val outFile: String,
val name: String?,
val includeBinary: Boolean
) )
companion object : Compile() 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 asmble.util.Logger
import kotlin.system.exitProcess 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>) { fun main(args: Array<String>) {
if (args.isEmpty()) return println( if (args.isEmpty()) return println(

View File

@ -9,6 +9,9 @@ import org.objectweb.asm.tree.*
import org.objectweb.asm.util.TraceClassVisitor import org.objectweb.asm.util.TraceClassVisitor
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter 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.KClass
import kotlin.reflect.KFunction import kotlin.reflect.KFunction
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@ -60,6 +63,13 @@ val Class<*>.valueType: Node.Type.Value? get() = when (this) {
else -> error("Unrecognized value type class: $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<*>.declarer: Class<*> get() = this.javaField!!.declaringClass
val KProperty<*>.asmDesc: String get() = Type.getDescriptor(this.javaField!!.type) 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() = val Node.Type.Func.asmDesc: String get() =
(this.ret?.typeRef ?: Void::class.ref).asMethodRetDesc(*this.params.map { it.typeRef }.toTypedArray()) (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. // 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 // See $func12 of block.wast. I don't believe the extra time over the
// instructions to remove the NOPs is worth it. // instructions to remove the NOPs is worth it.
val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES + ClassWriter.COMPUTE_MAXS)
this.accept(cw) this.accept(cw)
return cw.toByteArray() return cw.toByteArray()
} }

View File

@ -1,10 +1,17 @@
package asmble.compile.jvm 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.ast.Node
import asmble.io.AstToBinary
import asmble.io.ByteWriter
import asmble.util.Either import asmble.util.Either
import org.objectweb.asm.Opcodes import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type import org.objectweb.asm.Type
import org.objectweb.asm.tree.* import org.objectweb.asm.tree.*
import java.io.ByteArrayOutputStream
import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles import java.lang.invoke.MethodHandles
@ -13,12 +20,13 @@ open class AstToAsm {
fun fromModule(ctx: ClsContext) { fun fromModule(ctx: ClsContext) {
// Invoke dynamic among other things // Invoke dynamic among other things
ctx.cls.superName = Object::class.ref.asmName 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 ctx.cls.access += Opcodes.ACC_PUBLIC
addFields(ctx) addFields(ctx)
addConstructors(ctx) addConstructors(ctx)
addFuncs(ctx) addFuncs(ctx)
addExports(ctx) addExports(ctx)
addAnnotations(ctx)
} }
fun addFields(ctx: ClsContext) { fun addFields(ctx: ClsContext) {
@ -85,7 +93,7 @@ open class AstToAsm {
func = initializeConstructorTables(ctx, func, 0) func = initializeConstructorTables(ctx, func, 0)
func = executeConstructorStartFunction(ctx, func, 0) func = executeConstructorStartFunction(ctx, func, 0)
func = func.addInsns(InsnNode(Opcodes.RETURN)) func = func.addInsns(InsnNode(Opcodes.RETURN))
ctx.cls.methods.add(func.toMethodNode()) ctx.cls.methods.add(toConstructorNode(ctx, func))
} }
fun addMaxMemConstructor(ctx: ClsContext) { fun addMaxMemConstructor(ctx: ClsContext) {
@ -107,7 +115,7 @@ open class AstToAsm {
MethodInsnNode(Opcodes.INVOKESPECIAL, ctx.thisRef.asmName, "<init>", desc, false), MethodInsnNode(Opcodes.INVOKESPECIAL, ctx.thisRef.asmName, "<init>", desc, false),
InsnNode(Opcodes.RETURN) InsnNode(Opcodes.RETURN)
) )
ctx.cls.methods.add(func.toMethodNode()) ctx.cls.methods.add(toConstructorNode(ctx, func))
} }
fun addMemClassConstructor(ctx: ClsContext) { fun addMemClassConstructor(ctx: ClsContext) {
@ -148,7 +156,7 @@ open class AstToAsm {
func = executeConstructorStartFunction(ctx, func, 1) func = executeConstructorStartFunction(ctx, func, 1)
func = func.addInsns(InsnNode(Opcodes.RETURN)) func = func.addInsns(InsnNode(Opcodes.RETURN))
ctx.cls.methods.add(func.toMethodNode()) ctx.cls.methods.add(toConstructorNode(ctx, func))
} }
fun addMemDefaultConstructor(ctx: ClsContext) { fun addMemDefaultConstructor(ctx: ClsContext) {
@ -167,7 +175,7 @@ open class AstToAsm {
MethodInsnNode(Opcodes.INVOKESPECIAL, ctx.thisRef.asmName, "<init>", desc, false), MethodInsnNode(Opcodes.INVOKESPECIAL, ctx.thisRef.asmName, "<init>", desc, false),
InsnNode(Opcodes.RETURN) InsnNode(Opcodes.RETURN)
) )
ctx.cls.methods.add(func.toMethodNode()) ctx.cls.methods.add(toConstructorNode(ctx, func))
} }
fun constructorImportTypes(ctx: ClsContext) = fun constructorImportTypes(ctx: ClsContext) =
@ -176,6 +184,61 @@ open class AstToAsm {
ctx.importGlobals.map { MethodHandle::class.ref } + ctx.importGlobals.map { MethodHandle::class.ref } +
ctx.mod.imports.filter { it.kind is Node.Import.Kind.Table }.map { Array<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) = fun setConstructorGlobalImports(ctx: ClsContext, func: Func, paramsBeforeImports: Int) =
ctx.importGlobals.indices.fold(func) { func, importIndex -> ctx.importGlobals.indices.fold(func) { func, importIndex ->
func.addInsns( 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) { fun addExportFunc(ctx: ClsContext, export: Node.Export) {
val funcType = ctx.funcTypeAtIndex(export.index) val funcType = ctx.funcTypeAtIndex(export.index)
val method = MethodNode(Opcodes.ACC_PUBLIC, export.field.javaIdent, funcType.asmDesc, null, null) 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.F32 -> Opcodes.FRETURN
Node.Type.Value.F64 -> Opcodes.DRETURN Node.Type.Value.F64 -> Opcodes.DRETURN
})) }))
method.visibleAnnotations = listOf(exportAnnotation(export))
ctx.cls.methods.plusAssign(method) ctx.cls.methods.plusAssign(method)
} }
@ -481,6 +552,7 @@ open class AstToAsm {
Node.Type.Value.F32 -> Opcodes.FRETURN Node.Type.Value.F32 -> Opcodes.FRETURN
Node.Type.Value.F64 -> Opcodes.DRETURN Node.Type.Value.F64 -> Opcodes.DRETURN
})) }))
method.visibleAnnotations = listOf(exportAnnotation(export))
ctx.cls.methods.plusAssign(method) ctx.cls.methods.plusAssign(method)
} }
@ -494,6 +566,7 @@ open class AstToAsm {
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName, "memory", ctx.mem.memType.asmDesc), FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName, "memory", ctx.mem.memType.asmDesc),
InsnNode(Opcodes.ARETURN) InsnNode(Opcodes.ARETURN)
) )
method.visibleAnnotations = listOf(exportAnnotation(export))
ctx.cls.methods.plusAssign(method) ctx.cls.methods.plusAssign(method)
} }
@ -507,6 +580,7 @@ open class AstToAsm {
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName, "table", Array<MethodHandle>::class.ref.asmDesc), FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName, "table", Array<MethodHandle>::class.ref.asmDesc),
InsnNode(Opcodes.ARETURN) InsnNode(Opcodes.ARETURN)
) )
method.visibleAnnotations = listOf(exportAnnotation(export))
ctx.cls.methods.plusAssign(method) 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() companion object : AstToAsm()
} }

View File

@ -16,6 +16,7 @@ data class ClsContext(
val mod: Node.Module, val mod: Node.Module,
val cls: ClassNode = ClassNode().also { it.name = (packageName.replace('.', '/') + "/$className").trimStart('/') }, val cls: ClassNode = ClassNode().also { it.name = (packageName.replace('.', '/') + "/$className").trimStart('/') },
val mem: Mem = ByteBufferMem, val mem: Mem = ByteBufferMem,
val modName: String? = null,
val reworker: InsnReworker = InsnReworker, val reworker: InsnReworker = InsnReworker,
val logger: Logger = Logger.Print(Logger.Level.OFF), val logger: Logger = Logger.Print(Logger.Level.OFF),
val funcBuilder: FuncBuilder = FuncBuilder, val funcBuilder: FuncBuilder = FuncBuilder,
@ -26,7 +27,8 @@ data class ClsContext(
val preventMemIndexOverflow: Boolean = false, val preventMemIndexOverflow: Boolean = false,
val accurateNanBits: Boolean = true, val accurateNanBits: Boolean = true,
val checkSignedDivIntegerOverflow: Boolean = true, val checkSignedDivIntegerOverflow: Boolean = true,
val jumpTableChunkSize: Int = 5000 val jumpTableChunkSize: Int = 5000,
val includeBinary: Boolean = false
) : Logger by logger { ) : Logger by logger {
val importFuncs: List<Node.Import> by lazy { mod.imports.filter { it.kind is Node.Import.Kind.Func } } 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 } } 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 package asmble.run.jvm
import asmble.annotation.WasmName import asmble.annotation.WasmExport
import asmble.annotation.WasmExternalKind
import asmble.ast.Node import asmble.ast.Node
import asmble.compile.jvm.Mem import asmble.compile.jvm.Mem
import asmble.compile.jvm.ref import asmble.compile.jvm.ref
@ -8,13 +9,25 @@ import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType import java.lang.invoke.MethodType
import java.lang.reflect.Constructor import java.lang.reflect.Constructor
import java.lang.reflect.Modifier
interface Module { 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 { data class Composite(val modules: List<Module>) : Module {
override fun bindMethod(ctx: ScriptContext, wasmName: String, javaName: String, type: MethodType) = override fun bindMethod(
modules.asSequence().mapNotNull { it.bindMethod(ctx, wasmName, javaName, type) }.singleOrNull() ctx: ScriptContext,
wasmName: String,
wasmKind: WasmExternalKind,
javaName: String,
type: MethodType
) = modules.asSequence().mapNotNull { it.bindMethod(ctx, wasmName, wasmKind, javaName, type) }.singleOrNull()
} }
interface Instance : Module { interface Instance : Module {
@ -22,16 +35,22 @@ interface Module {
// Guaranteed to be the same instance when there is no error // Guaranteed to be the same instance when there is no error
fun instance(ctx: ScriptContext): Any fun instance(ctx: ScriptContext): Any
override fun bindMethod(ctx: ScriptContext, wasmName: String, javaName: String, type: MethodType) = override fun bindMethod(
try { ctx: ScriptContext,
MethodHandles.lookup().bind(instance(ctx), javaName, type) wasmName: String,
} catch (_: NoSuchMethodException) { wasmKind: WasmExternalKind,
// Try any method w/ the proper annotation javaName: String,
cls.methods.mapNotNull { method -> type: MethodType
if (method.getAnnotation(WasmName::class.java)?.value != wasmName) null ) = cls.methods.filter {
else MethodHandles.lookup().unreflect(method).bindTo(instance(ctx)).takeIf { it.type() == type } // @WasmExport match or just javaName match
}.singleOrNull() 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 { data class Native(override val cls: Class<*>, val inst: Any) : Instance {

View File

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

View File

@ -1,6 +1,7 @@
package asmble.run.jvm package asmble.run.jvm
import asmble.annotation.WasmName import asmble.annotation.WasmExport
import asmble.annotation.WasmExternalKind
import asmble.compile.jvm.Mem import asmble.compile.jvm.Mem
import java.io.PrintWriter import java.io.PrintWriter
import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandle
@ -11,10 +12,10 @@ open class TestHarness(val out: PrintWriter) {
// WASM is evil, not me: // WASM is evil, not me:
// https://github.com/WebAssembly/spec/blob/6a01dab6d29b7c2b5dfd3bb3879bbd6ab76fd5dc/interpreter/host/import/spectest.ml#L12 // https://github.com/WebAssembly/spec/blob/6a01dab6d29b7c2b5dfd3bb3879bbd6ab76fd5dc/interpreter/host/import/spectest.ml#L12
@get:WasmName("global") val globalInt = 666 @get:WasmExport("global", kind = WasmExternalKind.GLOBAL) val globalInt = 666
@get:WasmName("global") val globalLong = 666L @get:WasmExport("global", kind = WasmExternalKind.GLOBAL) val globalLong = 666L
@get:WasmName("global") val globalFloat = 666.6f @get:WasmExport("global", kind = WasmExternalKind.GLOBAL) val globalFloat = 666.6f
@get:WasmName("global") val globalDouble = 666.6 @get:WasmExport("global", kind = WasmExternalKind.GLOBAL) val globalDouble = 666.6
val table = arrayOfNulls<MethodHandle>(10) val table = arrayOfNulls<MethodHandle>(10)
val memory = ByteBuffer. val memory = ByteBuffer.
allocateDirect(2 * Mem.PAGE_SIZE). 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) { override fun warningInsteadOfErrReason(t: Throwable) = when (name) {
// NaN bit patterns can be off // 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 if (isNanMismatch(t)) "NaN JVM bit patterns can be off" else null
// We don't hold table capacity right now // We don't hold table capacity right now
// TODO: Figure out how we want to store/retrieve 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.BaseTestUnit
import asmble.TestBase import asmble.TestBase
import asmble.annotation.WasmModule
import asmble.io.AstToBinary
import asmble.io.AstToSExpr import asmble.io.AstToSExpr
import asmble.io.ByteWriter
import asmble.io.SExprToStr import asmble.io.SExprToStr
import org.junit.Assume import org.junit.Assume
import org.junit.Test import org.junit.Test
@ -36,7 +39,9 @@ abstract class TestRunner<out T : BaseTestUnit>(val unit: T) : TestBase() {
packageName = unit.packageName, packageName = unit.packageName,
logger = this, logger = this,
adjustContext = { it.copy(eagerFailLargeMemOffset = false) }, 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)) ).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 // 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 // Sadly, sometimes the expected output is trimmed in Emscripten tests
assertEquals(it.trimEnd(), out.toByteArray().toString(Charsets.UTF_8).trimEnd()) 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; package run.jvm.emscripten;
import asmble.annotation.WasmName; import asmble.annotation.WasmExport;
public class Common { public class Common {
private final Env env; private final Env env;
@ -13,7 +13,7 @@ public class Common {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@WasmName("__assert_fail") @WasmExport("__assert_fail")
public void assertFail(int conditionPtr, int filenamePtr, int line, int funcPtr) { public void assertFail(int conditionPtr, int filenamePtr, int line, int funcPtr) {
throw new AssertionError("Assertion failed: " + env.mem.getCString(conditionPtr) + ", at " + throw new AssertionError("Assertion failed: " + env.mem.getCString(conditionPtr) + ", at " +
env.mem.getCString(filenamePtr) + ":" + line + ", func " + env.mem.getCString(funcPtr)); env.mem.getCString(filenamePtr) + ":" + line + ", func " + env.mem.getCString(funcPtr));
@ -29,12 +29,12 @@ public class Common {
return 0; return 0;
} }
@WasmName("__cxa_call_unexpected") @WasmExport("__cxa_call_unexpected")
public void callUnexpected(int ex) { public void callUnexpected(int ex) {
throw new EmscriptenException("Unexpected: " + ex); throw new EmscriptenException("Unexpected: " + ex);
} }
@WasmName("__lock") @WasmExport("__lock")
public void lock(int arg) { public void lock(int arg) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@ -43,7 +43,7 @@ public class Common {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@WasmName("__unlock") @WasmExport("__unlock")
public void unlock(int arg) { public void unlock(int arg) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View File

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