8 Commits
0.2.0 ... 0.3.0

16 changed files with 185 additions and 86 deletions

View File

@ -256,15 +256,16 @@ In the WebAssembly MVP a table is just a set of function pointers. This is store
#### Globals #### Globals
Globals are stored as fields on the class. A non-import global is simply a field, but an import global is a Globals are stored as fields on the class. A non-import global is simply a field that is final if not mutable. An import
`MethodHandle` to the getter (and would be a `MethodHandle` to the setter if mutable globals were supported). Any values global is a `MethodHandle` to the getter and a `MethodHandle` to the setter if mutable. Any values for the globals are
for the globals are set in the constructor. set in the constructor.
#### Imports #### Imports
The constructor accepts all imports as params. Memory is imported via a `ByteBuffer` param, then function The constructor accepts all imports as params. Memory is imported via a `ByteBuffer` param, then function
imports as `MethodHandle` params, then global imports as `MethodHandle` params, then a `MethodHandle` array param for an imports as `MethodHandle` params, then global imports as `MethodHandle` params (one for getter and another for setter if
imported table. All of these values are set as fields in the constructor. mutable), then a `MethodHandle` array param for an imported table. All of these values are set as fields in the
constructor.
#### Exports #### Exports
@ -363,9 +364,9 @@ stack (e.g. some places where we do a swap).
Below are some performance and implementation quirks where there is a bit of an impedance mismatch between WebAssembly Below are some performance and implementation quirks where there is a bit of an impedance mismatch between WebAssembly
and the JVM: and the JVM:
* WebAssembly has a nice data section for byte arrays whereas the JVM does not. Right now we build a byte array from * WebAssembly has a nice data section for byte arrays whereas the JVM does not. Right now we use a single-byte-char
a bunch of consts at runtime which is multiple operations per byte. This can bloat the class file size, but is quite string constant (i.e. ISO-8859 charset). This saves class file size, but this means we call `String::getBytes` on
fast compared to alternatives such as string constants. init to load bytes from the string constant.
* The JVM makes no guarantees about trailing bits being preserved on NaN floating point representations like WebAssembly * The JVM makes no guarantees about trailing bits being preserved on NaN floating point representations like WebAssembly
does. This causes some mismatch on WebAssembly tests depending on how the JVM "feels" (I haven't dug into why some does. This causes some mismatch on WebAssembly tests depending on how the JVM "feels" (I haven't dug into why some
bit patterns stay and some don't when NaNs are passed through methods). bit patterns stay and some don't when NaNs are passed through methods).
@ -417,6 +418,8 @@ WASM compiled from Rust, C, Java, etc if e.g. they all have their own way of han
definition of an importable set of modules that does all of these things, even if it's in WebIDL. I dunno, maybe the definition of an importable set of modules that does all of these things, even if it's in WebIDL. I dunno, maybe the
effort is already there, I haven't really looked. effort is already there, I haven't really looked.
There is https://github.com/konsoletyper/teavm
**So I can compile something in C via Emscripten and have it run on the JVM with this?** **So I can compile something in C via Emscripten and have it run on the JVM with this?**
Yes, but work is required. WebAssembly is lacking any kind of standard library. So Emscripten will either embed it or Yes, but work is required. WebAssembly is lacking any kind of standard library. So Emscripten will either embed it or

View File

@ -13,4 +13,5 @@ public @interface WasmImport {
WasmExternalKind kind(); WasmExternalKind kind();
int resizableLimitInitial() default -1; int resizableLimitInitial() default -1;
int resizableLimitMaximum() default -1; int resizableLimitMaximum() default -1;
boolean globalSetter() default false;
} }

View File

@ -2,7 +2,7 @@ group 'asmble'
version '0.2.0' version '0.2.0'
buildscript { buildscript {
ext.kotlin_version = '1.2.30' ext.kotlin_version = '1.2.51'
ext.asm_version = '5.2' ext.asm_version = '5.2'
repositories { repositories {

View File

@ -223,8 +223,8 @@ sealed class Node {
data class I64Store8(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset data class I64Store8(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
data class I64Store16(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset data class I64Store16(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
data class I64Store32(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset data class I64Store32(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
data class CurrentMemory(override val reserved: Boolean) : Instr(), Args.Reserved data class MemorySize(override val reserved: Boolean) : Instr(), Args.Reserved
data class GrowMemory(override val reserved: Boolean) : Instr(), Args.Reserved data class MemoryGrow(override val reserved: Boolean) : Instr(), Args.Reserved
// Constants // Constants
data class I32Const(override val value: Int) : Instr(), Args.Const<Int> data class I32Const(override val value: Int) : Instr(), Args.Const<Int>
@ -511,8 +511,8 @@ sealed class Node {
opMapEntry("i64.store8", 0x3c, ::MemOpAlignOffsetArg, Instr::I64Store8, Instr.I64Store8::class) opMapEntry("i64.store8", 0x3c, ::MemOpAlignOffsetArg, Instr::I64Store8, Instr.I64Store8::class)
opMapEntry("i64.store16", 0x3d, ::MemOpAlignOffsetArg, Instr::I64Store16, Instr.I64Store16::class) opMapEntry("i64.store16", 0x3d, ::MemOpAlignOffsetArg, Instr::I64Store16, Instr.I64Store16::class)
opMapEntry("i64.store32", 0x3e, ::MemOpAlignOffsetArg, Instr::I64Store32, Instr.I64Store32::class) opMapEntry("i64.store32", 0x3e, ::MemOpAlignOffsetArg, Instr::I64Store32, Instr.I64Store32::class)
opMapEntry("current_memory", 0x3f, ::MemOpReservedArg, Instr::CurrentMemory, Instr.CurrentMemory::class) opMapEntry("memory.size", 0x3f, ::MemOpReservedArg, Instr::MemorySize, Instr.MemorySize::class)
opMapEntry("grow_memory", 0x40, ::MemOpReservedArg, Instr::GrowMemory, Instr.GrowMemory::class) opMapEntry("memory.grow", 0x40, ::MemOpReservedArg, Instr::MemoryGrow, Instr.MemoryGrow::class)
opMapEntry("i32.const", 0x41, ::ConstOpIntArg, Instr::I32Const, Instr.I32Const::class) opMapEntry("i32.const", 0x41, ::ConstOpIntArg, Instr::I32Const, Instr.I32Const::class)
opMapEntry("i64.const", 0x42, ::ConstOpLongArg, Instr::I64Const, Instr.I64Const::class) opMapEntry("i64.const", 0x42, ::ConstOpLongArg, Instr::I64Const, Instr.I64Const::class)

View File

@ -47,10 +47,13 @@ open class AstToAsm {
}) })
// Now all import globals as getter (and maybe setter) method handles // Now all import globals as getter (and maybe setter) method handles
ctx.cls.fields.addAll(ctx.importGlobals.mapIndexed { index, import -> ctx.cls.fields.addAll(ctx.importGlobals.mapIndexed { index, import ->
if ((import.kind as Node.Import.Kind.Global).type.mutable) throw CompileErr.MutableGlobalImport(index) val getter = FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, ctx.importGlobalGetterFieldName(index),
FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, ctx.importGlobalGetterFieldName(index),
MethodHandle::class.ref.asmDesc, null, null) MethodHandle::class.ref.asmDesc, null, null)
}) if (!(import.kind as Node.Import.Kind.Global).type.mutable) listOf(getter)
else listOf(getter, FieldNode(
Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, ctx.importGlobalSetterFieldName(index),
MethodHandle::class.ref.asmDesc, null, null))
}.flatten())
// Now all non-import globals // Now all non-import globals
ctx.cls.fields.addAll(ctx.mod.globals.mapIndexed { index, global -> ctx.cls.fields.addAll(ctx.mod.globals.mapIndexed { index, global ->
val access = Opcodes.ACC_PRIVATE + if (!global.type.mutable) Opcodes.ACC_FINAL else 0 val access = Opcodes.ACC_PRIVATE + if (!global.type.mutable) Opcodes.ACC_FINAL else 0
@ -180,9 +183,11 @@ open class AstToAsm {
fun constructorImportTypes(ctx: ClsContext) = fun constructorImportTypes(ctx: ClsContext) =
ctx.importFuncs.map { MethodHandle::class.ref } + ctx.importFuncs.map { MethodHandle::class.ref } +
// We know it's only getters ctx.importGlobals.flatMap {
ctx.importGlobals.map { MethodHandle::class.ref } + // If it's mutable, it also comes with a setter
ctx.mod.imports.filter { it.kind is Node.Import.Kind.Table }.map { Array<MethodHandle>::class.ref } if ((it.kind as? Node.Import.Kind.Global)?.type?.mutable == false) listOf(MethodHandle::class.ref)
else listOf(MethodHandle::class.ref, 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 -> fun toConstructorNode(ctx: ClsContext, func: Func) = mutableListOf<List<AnnotationNode>>().let { paramAnns ->
// If the first param is a mem class and imported, add annotation // If the first param is a mem class and imported, add annotation
@ -199,7 +204,15 @@ open class AstToAsm {
} }
// All non-mem imports one after another // All non-mem imports one after another
ctx.importFuncs.forEach { paramAnns.add(listOf(importAnnotation(ctx, it))) } ctx.importFuncs.forEach { paramAnns.add(listOf(importAnnotation(ctx, it))) }
ctx.importGlobals.forEach { paramAnns.add(listOf(importAnnotation(ctx, it))) } ctx.importGlobals.forEach {
paramAnns.add(listOf(importAnnotation(ctx, it)))
// There are two annotations here if it's mutable
if ((it.kind as? Node.Import.Kind.Global)?.type?.mutable == true)
paramAnns.add(listOf(importAnnotation(ctx, it).also {
it.values.add("globalSetter")
it.values.add(true)
}))
}
ctx.mod.imports.forEach { ctx.mod.imports.forEach {
if (it.kind is Node.Import.Kind.Table) paramAnns.add(listOf(importAnnotation(ctx, it))) if (it.kind is Node.Import.Kind.Table) paramAnns.add(listOf(importAnnotation(ctx, it)))
} }
@ -240,14 +253,25 @@ open class AstToAsm {
} }
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.foldIndexed(func to ctx.importFuncs.size + paramsBeforeImports) {
importIndex, (func, importParamOffset), import ->
// Always a getter handle
func.addInsns( func.addInsns(
VarInsnNode(Opcodes.ALOAD, 0), VarInsnNode(Opcodes.ALOAD, 0),
VarInsnNode(Opcodes.ALOAD, ctx.importFuncs.size + importIndex + paramsBeforeImports + 1), VarInsnNode(Opcodes.ALOAD, importParamOffset + 1),
FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName, FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName,
ctx.importGlobalGetterFieldName(importIndex), MethodHandle::class.ref.asmDesc) ctx.importGlobalGetterFieldName(importIndex), MethodHandle::class.ref.asmDesc)
) ).let { func ->
// If it's mutable, it has a second setter handle
if ((import.kind as? Node.Import.Kind.Global)?.type?.mutable == false) func to importParamOffset + 1
else func.addInsns(
VarInsnNode(Opcodes.ALOAD, 0),
VarInsnNode(Opcodes.ALOAD, importParamOffset + 2),
FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName,
ctx.importGlobalSetterFieldName(importIndex), MethodHandle::class.ref.asmDesc)
) to importParamOffset + 2
} }
}.first
fun setConstructorFunctionImports(ctx: ClsContext, func: Func, paramsBeforeImports: Int) = fun setConstructorFunctionImports(ctx: ClsContext, func: Func, paramsBeforeImports: Int) =
ctx.importFuncs.indices.fold(func) { func, importIndex -> ctx.importFuncs.indices.fold(func) { func, importIndex ->
@ -261,7 +285,10 @@ open class AstToAsm {
fun setConstructorTableImports(ctx: ClsContext, func: Func, paramsBeforeImports: Int) = fun setConstructorTableImports(ctx: ClsContext, func: Func, paramsBeforeImports: Int) =
if (ctx.mod.imports.none { it.kind is Node.Import.Kind.Table }) func else { if (ctx.mod.imports.none { it.kind is Node.Import.Kind.Table }) func else {
val importIndex = ctx.importFuncs.size + ctx.importGlobals.size + paramsBeforeImports + 1 val importIndex = ctx.importFuncs.size +
// Mutable global imports have setters and take up two spots
ctx.importGlobals.sumBy { if ((it.kind as? Node.Import.Kind.Global)?.type?.mutable == true) 2 else 1 } +
paramsBeforeImports + 1
func.addInsns( func.addInsns(
VarInsnNode(Opcodes.ALOAD, 0), VarInsnNode(Opcodes.ALOAD, 0),
VarInsnNode(Opcodes.ALOAD, importIndex), VarInsnNode(Opcodes.ALOAD, importIndex),
@ -299,11 +326,14 @@ open class AstToAsm {
global.type.contentType.typeRef, global.type.contentType.typeRef,
refGlobalKind.type.contentType.typeRef refGlobalKind.type.contentType.typeRef
) )
val paramOffset = ctx.importFuncs.size + paramsBeforeImports + 1 +
ctx.importGlobals.take(it.index).sumBy {
// Immutable jumps 1, mutable jumps 2
if ((it.kind as? Node.Import.Kind.Global)?.type?.mutable == false) 1
else 2
}
listOf( listOf(
VarInsnNode( VarInsnNode(Opcodes.ALOAD, paramOffset),
Opcodes.ALOAD,
ctx.importFuncs.size + it.index + paramsBeforeImports + 1
),
MethodInsnNode( MethodInsnNode(
Opcodes.INVOKEVIRTUAL, Opcodes.INVOKEVIRTUAL,
MethodHandle::class.ref.asmName, MethodHandle::class.ref.asmName,
@ -356,7 +386,10 @@ open class AstToAsm {
// Otherwise, it was imported and we can set the elems on the imported one // Otherwise, it was imported and we can set the elems on the imported one
// from the parameter // from the parameter
// TODO: I think this is a security concern and bad practice, may revisit // TODO: I think this is a security concern and bad practice, may revisit
val importIndex = ctx.importFuncs.size + ctx.importGlobals.size + paramsBeforeImports + 1 val importIndex = ctx.importFuncs.size + ctx.importGlobals.sumBy {
// Immutable is 1, mutable is 2
if ((it.kind as? Node.Import.Kind.Global)?.type?.mutable == false) 1 else 2
} + paramsBeforeImports + 1
return func.addInsns(VarInsnNode(Opcodes.ALOAD, importIndex)). return func.addInsns(VarInsnNode(Opcodes.ALOAD, importIndex)).
let { func -> addElemsToTable(ctx, func, paramsBeforeImports) }. let { func -> addElemsToTable(ctx, func, paramsBeforeImports) }.
// Remove the array that's still there // Remove the array that's still there
@ -532,28 +565,58 @@ open class AstToAsm {
is Either.Left -> (global.v.kind as Node.Import.Kind.Global).type is Either.Left -> (global.v.kind as Node.Import.Kind.Global).type
is Either.Right -> global.v.type is Either.Right -> global.v.type
} }
if (type.mutable) throw CompileErr.MutableGlobalExport(export.index)
// Create a simple getter // Create a simple getter
val method = MethodNode(Opcodes.ACC_PUBLIC, "get" + export.field.javaIdent.capitalize(), val getter = MethodNode(Opcodes.ACC_PUBLIC, "get" + export.field.javaIdent.capitalize(),
"()" + type.contentType.typeRef.asmDesc, null, null) "()" + type.contentType.typeRef.asmDesc, null, null)
method.addInsns(VarInsnNode(Opcodes.ALOAD, 0)) getter.addInsns(VarInsnNode(Opcodes.ALOAD, 0))
if (global is Either.Left) method.addInsns( if (global is Either.Left) getter.addInsns(
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName, FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName,
ctx.importGlobalGetterFieldName(export.index), MethodHandle::class.ref.asmDesc), ctx.importGlobalGetterFieldName(export.index), MethodHandle::class.ref.asmDesc),
MethodInsnNode(Opcodes.INVOKEVIRTUAL, MethodHandle::class.ref.asmName, "invokeExact", MethodInsnNode(Opcodes.INVOKEVIRTUAL, MethodHandle::class.ref.asmName, "invokeExact",
"()" + type.contentType.typeRef.asmDesc, false) "()" + type.contentType.typeRef.asmDesc, false)
) else method.addInsns( ) else getter.addInsns(
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName, ctx.globalName(export.index), FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName, ctx.globalName(export.index),
type.contentType.typeRef.asmDesc) type.contentType.typeRef.asmDesc)
) )
method.addInsns(InsnNode(when (type.contentType) { getter.addInsns(InsnNode(when (type.contentType) {
Node.Type.Value.I32 -> Opcodes.IRETURN Node.Type.Value.I32 -> Opcodes.IRETURN
Node.Type.Value.I64 -> Opcodes.LRETURN Node.Type.Value.I64 -> Opcodes.LRETURN
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)) getter.visibleAnnotations = listOf(exportAnnotation(export))
ctx.cls.methods.plusAssign(method) ctx.cls.methods.plusAssign(getter)
// If mutable, create simple setter
if (type.mutable) {
val setter = MethodNode(Opcodes.ACC_PUBLIC, "set" + export.field.javaIdent.capitalize(),
"(${type.contentType.typeRef.asmDesc})V", null, null)
setter.addInsns(VarInsnNode(Opcodes.ALOAD, 0))
if (global is Either.Left) setter.addInsns(
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName,
ctx.importGlobalSetterFieldName(export.index), MethodHandle::class.ref.asmDesc),
VarInsnNode(when (type.contentType) {
Node.Type.Value.I32 -> Opcodes.ILOAD
Node.Type.Value.I64 -> Opcodes.LLOAD
Node.Type.Value.F32 -> Opcodes.FLOAD
Node.Type.Value.F64 -> Opcodes.DLOAD
}, 1),
MethodInsnNode(Opcodes.INVOKEVIRTUAL, MethodHandle::class.ref.asmName, "invokeExact",
"(${type.contentType.typeRef.asmDesc})V", false),
InsnNode(Opcodes.RETURN)
) else setter.addInsns(
VarInsnNode(when (type.contentType) {
Node.Type.Value.I32 -> Opcodes.ILOAD
Node.Type.Value.I64 -> Opcodes.LLOAD
Node.Type.Value.F32 -> Opcodes.FLOAD
Node.Type.Value.F64 -> Opcodes.DLOAD
}, 1),
FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName, ctx.globalName(export.index),
type.contentType.typeRef.asmDesc),
InsnNode(Opcodes.RETURN)
)
setter.visibleAnnotations = listOf(exportAnnotation(export))
ctx.cls.methods.plusAssign(setter)
}
} }
fun addExportMemory(ctx: ClsContext, export: Node.Export) { fun addExportMemory(ctx: ClsContext, export: Node.Export) {

View File

@ -102,18 +102,6 @@ sealed class CompileErr(message: String, cause: Throwable? = null) : RuntimeExce
override val asmErrString get() = "global is immutable" override val asmErrString get() = "global is immutable"
} }
class MutableGlobalImport(
val index: Int
) : CompileErr("Attempted to import mutable global at index $index") {
override val asmErrString get() = "mutable globals cannot be imported"
}
class MutableGlobalExport(
val index: Int
) : CompileErr("Attempted to export global $index which is mutable") {
override val asmErrString get() = "mutable globals cannot be exported"
}
class GlobalInitNotConstant( class GlobalInitNotConstant(
val index: Int val index: Int
) : CompileErr("Expected init for global $index to be single constant value") { ) : CompileErr("Expected init for global $index to be single constant value") {

View File

@ -148,10 +148,10 @@ open class FuncBuilder {
is Node.Instr.I32Store8, is Node.Instr.I32Store16, is Node.Instr.I64Store8, is Node.Instr.I64Store16, is Node.Instr.I32Store8, is Node.Instr.I32Store16, is Node.Instr.I64Store8, is Node.Instr.I64Store16,
is Node.Instr.I64Store32 -> is Node.Instr.I64Store32 ->
applyStoreOp(ctx, fn, i as Node.Instr.Args.AlignOffset, index) applyStoreOp(ctx, fn, i as Node.Instr.Args.AlignOffset, index)
is Node.Instr.CurrentMemory -> is Node.Instr.MemorySize ->
applyCurrentMemory(ctx, fn) applyMemorySize(ctx, fn)
is Node.Instr.GrowMemory -> is Node.Instr.MemoryGrow ->
applyGrowMemory(ctx, fn) applyMemoryGrow(ctx, fn)
is Node.Instr.I32Const -> is Node.Instr.I32Const ->
fn.addInsns(i.value.const).push(Int::class.ref) fn.addInsns(i.value.const).push(Int::class.ref)
is Node.Instr.I64Const -> is Node.Instr.I64Const ->
@ -1062,14 +1062,14 @@ open class FuncBuilder {
).push(Int::class.ref) ).push(Int::class.ref)
} }
fun applyGrowMemory(ctx: FuncContext, fn: Func) = fun applyMemoryGrow(ctx: FuncContext, fn: Func) =
// Grow mem is a special case where the memory ref is already pre-injected on // Grow mem is a special case where the memory ref is already pre-injected on
// the stack before this call. Result is an int. // the stack before this call. Result is an int.
ctx.cls.assertHasMemory().let { ctx.cls.assertHasMemory().let {
ctx.cls.mem.growMemory(ctx, fn) ctx.cls.mem.growMemory(ctx, fn)
} }
fun applyCurrentMemory(ctx: FuncContext, fn: Func) = fun applyMemorySize(ctx: FuncContext, fn: Func) =
// Curr mem is not specially injected, so we have to put the memory on the // Curr mem is not specially injected, so we have to put the memory on the
// stack since we need it // stack since we need it
ctx.cls.assertHasMemory().let { ctx.cls.assertHasMemory().let {

View File

@ -194,7 +194,7 @@ open class InsnReworker {
is Node.Instr.I64Store32 -> is Node.Instr.I64Store32 ->
injectBeforeLastStackCount(Insn.MemNeededOnStack, 2) injectBeforeLastStackCount(Insn.MemNeededOnStack, 2)
// Grow memory requires "mem" before the single param // Grow memory requires "mem" before the single param
is Node.Instr.GrowMemory -> is Node.Instr.MemoryGrow ->
injectBeforeLastStackCount(Insn.MemNeededOnStack, 1) injectBeforeLastStackCount(Insn.MemNeededOnStack, 1)
else -> { } else -> { }
} }
@ -239,8 +239,8 @@ open class InsnReworker {
is Node.Instr.I32Store, is Node.Instr.I64Store, is Node.Instr.F32Store, is Node.Instr.F64Store, is Node.Instr.I32Store, is Node.Instr.I64Store, is Node.Instr.F32Store, is Node.Instr.F64Store,
is Node.Instr.I32Store8, is Node.Instr.I32Store16, is Node.Instr.I64Store8, is Node.Instr.I64Store16, is Node.Instr.I32Store8, is Node.Instr.I32Store16, is Node.Instr.I64Store8, is Node.Instr.I64Store16,
is Node.Instr.I64Store32 -> POP_PARAM is Node.Instr.I64Store32 -> POP_PARAM
is Node.Instr.CurrentMemory -> PUSH_RESULT is Node.Instr.MemorySize -> PUSH_RESULT
is Node.Instr.GrowMemory -> POP_PARAM + PUSH_RESULT is Node.Instr.MemoryGrow -> POP_PARAM + PUSH_RESULT
is Node.Instr.I32Const, is Node.Instr.I64Const, is Node.Instr.I32Const, is Node.Instr.I64Const,
is Node.Instr.F32Const, is Node.Instr.F64Const -> PUSH_RESULT is Node.Instr.F32Const, is Node.Instr.F64Const -> PUSH_RESULT
is Node.Instr.I32Add, is Node.Instr.I32Sub, is Node.Instr.I32Mul, is Node.Instr.I32DivS, is Node.Instr.I32Add, is Node.Instr.I32Sub, is Node.Instr.I32Mul, is Node.Instr.I32DivS,
@ -288,12 +288,12 @@ open class InsnReworker {
val inc = val inc =
if (lastCouldHaveMem) 0 if (lastCouldHaveMem) 0
else if (insn == Insn.MemNeededOnStack) 1 else if (insn == Insn.MemNeededOnStack) 1
else if (insn is Insn.Node && insn.insn is Node.Instr.CurrentMemory) 1 else if (insn is Insn.Node && insn.insn is Node.Instr.MemorySize) 1
else 0 else 0
val couldSetMemNext = if (insn !is Insn.Node) false else when (insn.insn) { val couldSetMemNext = if (insn !is Insn.Node) false else when (insn.insn) {
is Node.Instr.I32Store, is Node.Instr.I64Store, is Node.Instr.F32Store, is Node.Instr.F64Store, is Node.Instr.I32Store, is Node.Instr.I64Store, is Node.Instr.F32Store, is Node.Instr.F64Store,
is Node.Instr.I32Store8, is Node.Instr.I32Store16, is Node.Instr.I64Store8, is Node.Instr.I64Store16, is Node.Instr.I32Store8, is Node.Instr.I32Store16, is Node.Instr.I64Store8, is Node.Instr.I64Store16,
is Node.Instr.I64Store32, is Node.Instr.GrowMemory -> true is Node.Instr.I64Store32, is Node.Instr.MemoryGrow -> true
else -> false else -> false
} }
(count + inc) to couldSetMemNext (count + inc) to couldSetMemNext

View File

@ -144,8 +144,9 @@ open class BinaryToAst(
} }
} }
fun toLocals(b: ByteReader) = b.readVarUInt32AsInt().let { size -> fun toLocals(b: ByteReader): List<Node.Type.Value> {
toValueType(b).let { type -> List(size) { type } } val size = try { b.readVarUInt32AsInt() } catch (e: NumberFormatException) { throw IoErr.InvalidLocalSize(e) }
return toValueType(b).let { type -> List(size) { type } }
} }
fun toMemoryType(b: ByteReader) = Node.Type.Memory(toResizableLimits(b)) fun toMemoryType(b: ByteReader) = Node.Type.Memory(toResizableLimits(b))

View File

@ -1,12 +1,12 @@
package asmble.io package asmble.io
import asmble.util.toIntExact
import asmble.util.toUnsignedBigInt import asmble.util.toUnsignedBigInt
import asmble.util.toUnsignedLong import asmble.util.toUnsignedLong
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.math.BigInteger import java.math.BigInteger
abstract class ByteReader { abstract class ByteReader {
abstract val isEof: Boolean abstract val isEof: Boolean
@ -34,27 +34,30 @@ abstract class ByteReader {
} }
fun readVarInt7() = readSignedLeb128().let { fun readVarInt7() = readSignedLeb128().let {
require(it >= Byte.MIN_VALUE.toLong() && it <= Byte.MAX_VALUE.toLong()) if (it < Byte.MIN_VALUE.toLong() || it > Byte.MAX_VALUE.toLong()) throw IoErr.InvalidLeb128Number()
it.toByte() it.toByte()
} }
fun readVarInt32() = readSignedLeb128().toIntExact() fun readVarInt32() = readSignedLeb128().let {
if (it < Int.MIN_VALUE.toLong() || it > Int.MAX_VALUE.toLong()) throw IoErr.InvalidLeb128Number()
it.toInt()
}
fun readVarInt64() = readSignedLeb128() fun readVarInt64() = readSignedLeb128(9)
fun readVarUInt1() = readUnsignedLeb128().let { fun readVarUInt1() = readUnsignedLeb128().let {
require(it == 1 || it == 0) if (it != 1 && it != 0) throw IoErr.InvalidLeb128Number()
it == 1 it == 1
} }
fun readVarUInt7() = readUnsignedLeb128().let { fun readVarUInt7() = readUnsignedLeb128().let {
require(it <= 255) if (it > 255) throw IoErr.InvalidLeb128Number()
it.toShort() it.toShort()
} }
fun readVarUInt32() = readUnsignedLeb128().toUnsignedLong() fun readVarUInt32() = readUnsignedLeb128().toUnsignedLong()
protected fun readUnsignedLeb128(): Int { protected fun readUnsignedLeb128(maxCount: Int = 4): Int {
// Taken from Android source, Apache licensed // Taken from Android source, Apache licensed
var result = 0 var result = 0
var cur: Int var cur: Int
@ -63,12 +66,12 @@ abstract class ByteReader {
cur = readByte().toInt() and 0xff cur = readByte().toInt() and 0xff
result = result or ((cur and 0x7f) shl (count * 7)) result = result or ((cur and 0x7f) shl (count * 7))
count++ count++
} while (cur and 0x80 == 0x80 && count < 5) } while (cur and 0x80 == 0x80 && count <= maxCount)
if (cur and 0x80 == 0x80) throw NumberFormatException() if (cur and 0x80 == 0x80) throw IoErr.InvalidLeb128Number()
return result return result
} }
private fun readSignedLeb128(): Long { private fun readSignedLeb128(maxCount: Int = 4): Long {
// Taken from Android source, Apache licensed // Taken from Android source, Apache licensed
var result = 0L var result = 0L
var cur: Int var cur: Int
@ -79,8 +82,20 @@ abstract class ByteReader {
result = result or ((cur and 0x7f).toLong() shl (count * 7)) result = result or ((cur and 0x7f).toLong() shl (count * 7))
signBits = signBits shl 7 signBits = signBits shl 7
count++ count++
} while (cur and 0x80 == 0x80 && count < 10) } while (cur and 0x80 == 0x80 && count <= maxCount)
if (cur and 0x80 == 0x80) throw NumberFormatException() if (cur and 0x80 == 0x80) throw IoErr.InvalidLeb128Number()
// Check for 64 bit invalid, taken from Apache/MIT licensed:
// https://github.com/paritytech/parity-wasm/blob/2650fc14c458c6a252c9dc43dd8e0b14b6d264ff/src/elements/primitives.rs#L351
// TODO: probably need 32 bit checks too, but meh, not in the suite
if (count > maxCount && maxCount == 9) {
if (cur and 0b0100_0000 == 0b0100_0000) {
if ((cur or 0b1000_0000).toByte() != (-1).toByte()) throw IoErr.InvalidLeb128Number()
} else if (cur != 0) {
throw IoErr.InvalidLeb128Number()
}
}
if ((signBits shr 1) and result != 0L) result = result or signBits if ((signBits shr 1) and result != 0L) result = result or signBits
return result return result
} }

View File

@ -1,7 +1,6 @@
package asmble.io package asmble.io
import asmble.AsmErr import asmble.AsmErr
import java.math.BigInteger
sealed class IoErr(message: String, cause: Throwable? = null) : RuntimeException(message, cause), AsmErr { sealed class IoErr(message: String, cause: Throwable? = null) : RuntimeException(message, cause), AsmErr {
class UnexpectedEnd : IoErr("Unexpected EOF") { class UnexpectedEnd : IoErr("Unexpected EOF") {
@ -119,4 +118,13 @@ sealed class IoErr(message: String, cause: Throwable? = null) : RuntimeException
class InvalidUtf8Encoding : IoErr("Some byte sequence was not UTF-8 compatible") { class InvalidUtf8Encoding : IoErr("Some byte sequence was not UTF-8 compatible") {
override val asmErrString get() = "invalid UTF-8 encoding" override val asmErrString get() = "invalid UTF-8 encoding"
} }
class InvalidLeb128Number : IoErr("Invalid LEB128 number") {
override val asmErrString get() = "integer representation too long"
override val asmErrStrings get() = listOf(asmErrString, "integer too large")
}
class InvalidLocalSize(cause: NumberFormatException) : IoErr("Invalid local size", cause) {
override val asmErrString get() = "too many locals"
}
} }

View File

@ -116,9 +116,9 @@ interface Module {
} }
// Global imports // Global imports
val globalImports = mod.imports.mapNotNull { val globalImports = mod.imports.flatMap {
if (it.kind is Node.Import.Kind.Global) ctx.resolveImportGlobal(it, it.kind.type) if (it.kind is Node.Import.Kind.Global) ctx.resolveImportGlobals(it, it.kind.type)
else null else emptyList()
} }
constructorParams += globalImports constructorParams += globalImports

View File

@ -55,4 +55,12 @@ sealed class RunErr(message: String, cause: Throwable? = null) : RuntimeExceptio
override val asmErrString get() = "unknown import" override val asmErrString get() = "unknown import"
override val asmErrStrings get() = listOf(asmErrString, "incompatible import type") override val asmErrStrings get() = listOf(asmErrString, "incompatible import type")
} }
class ImportGlobalInvalidMutability(
val module: String,
val field: String,
val expected: Boolean
) : RunErr("Expected imported global $module::$field to have mutability as ${!expected}") {
override val asmErrString get() = "incompatible import type"
}
} }

View File

@ -263,10 +263,12 @@ data class ScriptContext(
return Module.Compiled(mod, classLoader.fromBuiltContext(ctx), name, ctx.mem) return Module.Compiled(mod, classLoader.fromBuiltContext(ctx), name, ctx.mem)
} }
fun bindImport(import: Node.Import, getter: Boolean, methodType: MethodType): MethodHandle { fun bindImport(import: Node.Import, getter: Boolean, methodType: MethodType) = bindImport(
import, if (getter) "get" + import.field.javaIdent.capitalize() else import.field.javaIdent, methodType)
fun bindImport(import: Node.Import, javaName: String, methodType: MethodType): MethodHandle {
// Find a method that matches our expectations // Find a method that matches our expectations
val module = registrations[import.module] ?: throw RunErr.ImportNotFound(import.module, import.field) val module = registrations[import.module] ?: throw RunErr.ImportNotFound(import.module, import.field)
val javaName = if (getter) "get" + import.field.javaIdent.capitalize() else import.field.javaIdent
val kind = when (import.kind) { val kind = when (import.kind) {
is Node.Import.Kind.Func -> WasmExternalKind.FUNCTION is Node.Import.Kind.Func -> WasmExternalKind.FUNCTION
is Node.Import.Kind.Table -> WasmExternalKind.TABLE is Node.Import.Kind.Table -> WasmExternalKind.TABLE
@ -281,8 +283,18 @@ data class ScriptContext(
bindImport(import, false, bindImport(import, false,
MethodType.methodType(funcType.ret?.jclass ?: Void.TYPE, funcType.params.map { it.jclass })) MethodType.methodType(funcType.ret?.jclass ?: Void.TYPE, funcType.params.map { it.jclass }))
fun resolveImportGlobal(import: Node.Import, globalType: Node.Type.Global) = fun resolveImportGlobals(import: Node.Import, globalType: Node.Type.Global): List<MethodHandle> {
bindImport(import, true, MethodType.methodType(globalType.contentType.jclass)) val getter = bindImport(import, true, MethodType.methodType(globalType.contentType.jclass))
// Whether the setter is present or not defines whether it is mutable
val setter = try {
bindImport(import, "set" + import.field.javaIdent.capitalize(),
MethodType.methodType(Void.TYPE, globalType.contentType.jclass))
} catch (e: RunErr.ImportNotFound) { null }
// Mutability must match
if (globalType.mutable == (setter == null))
throw RunErr.ImportGlobalInvalidMutability(import.module, import.field, globalType.mutable)
return if (setter == null) listOf(getter) else listOf(getter, setter)
}
fun resolveImportMemory(import: Node.Import, memoryType: Node.Type.Memory, mem: Mem) = fun resolveImportMemory(import: Node.Import, memoryType: Node.Type.Memory, mem: Mem) =
bindImport(import, true, MethodType.methodType(Class.forName(mem.memType.asm.className))). bindImport(import, true, MethodType.methodType(Class.forName(mem.memType.asm.className))).

View File

@ -14,7 +14,7 @@ class SpecTestUnit(name: String, wast: String, expectedOutput: String?) : BaseTe
override val defaultMaxMemPages get() = when (name) { override val defaultMaxMemPages get() = when (name) {
"nop" -> 20 "nop" -> 20
"resizing" -> 830 "memory_grow" -> 830
"imports" -> 5 "imports" -> 5
else -> 1 else -> 1
} }