mirror of
https://github.com/fluencelabs/asmble
synced 2025-07-04 00:41:34 +00:00
Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
dd33676e50 | |||
e9364574a3 | |||
73e6b5769a | |||
9d87ce440f | |||
51bc8008e1 | |||
198c521dd7 | |||
f24342959d | |||
368ab300fa |
19
README.md
19
README.md
@ -256,15 +256,16 @@ In the WebAssembly MVP a table is just a set of function pointers. This is store
|
||||
|
||||
#### Globals
|
||||
|
||||
Globals are stored as fields on the class. A non-import global is simply a field, but an import global is a
|
||||
`MethodHandle` to the getter (and would be a `MethodHandle` to the setter if mutable globals were supported). Any values
|
||||
for the globals are set in the constructor.
|
||||
Globals are stored as fields on the class. A non-import global is simply a field that is final if not mutable. An import
|
||||
global is a `MethodHandle` to the getter and a `MethodHandle` to the setter if mutable. Any values for the globals are
|
||||
set in the constructor.
|
||||
|
||||
#### Imports
|
||||
|
||||
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
|
||||
imported table. All of these values are set as fields in the constructor.
|
||||
imports as `MethodHandle` params, then global imports as `MethodHandle` params (one for getter and another for setter if
|
||||
mutable), then a `MethodHandle` array param for an imported table. All of these values are set as fields in the
|
||||
constructor.
|
||||
|
||||
#### 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
|
||||
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
|
||||
a bunch of consts at runtime which is multiple operations per byte. This can bloat the class file size, but is quite
|
||||
fast compared to alternatives such as string constants.
|
||||
* WebAssembly has a nice data section for byte arrays whereas the JVM does not. Right now we use a single-byte-char
|
||||
string constant (i.e. ISO-8859 charset). This saves class file size, but this means we call `String::getBytes` on
|
||||
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
|
||||
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).
|
||||
@ -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
|
||||
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?**
|
||||
|
||||
Yes, but work is required. WebAssembly is lacking any kind of standard library. So Emscripten will either embed it or
|
||||
|
@ -13,4 +13,5 @@ public @interface WasmImport {
|
||||
WasmExternalKind kind();
|
||||
int resizableLimitInitial() default -1;
|
||||
int resizableLimitMaximum() default -1;
|
||||
boolean globalSetter() default false;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ group 'asmble'
|
||||
version '0.2.0'
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.2.30'
|
||||
ext.kotlin_version = '1.2.51'
|
||||
ext.asm_version = '5.2'
|
||||
|
||||
repositories {
|
||||
|
@ -223,8 +223,8 @@ sealed class Node {
|
||||
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 I64Store32(override val align: Int, override val offset: Long) : Instr(), Args.AlignOffset
|
||||
data class CurrentMemory(override val reserved: Boolean) : Instr(), Args.Reserved
|
||||
data class GrowMemory(override val reserved: Boolean) : Instr(), Args.Reserved
|
||||
data class MemorySize(override val reserved: Boolean) : Instr(), Args.Reserved
|
||||
data class MemoryGrow(override val reserved: Boolean) : Instr(), Args.Reserved
|
||||
|
||||
// Constants
|
||||
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.store16", 0x3d, ::MemOpAlignOffsetArg, Instr::I64Store16, Instr.I64Store16::class)
|
||||
opMapEntry("i64.store32", 0x3e, ::MemOpAlignOffsetArg, Instr::I64Store32, Instr.I64Store32::class)
|
||||
opMapEntry("current_memory", 0x3f, ::MemOpReservedArg, Instr::CurrentMemory, Instr.CurrentMemory::class)
|
||||
opMapEntry("grow_memory", 0x40, ::MemOpReservedArg, Instr::GrowMemory, Instr.GrowMemory::class)
|
||||
opMapEntry("memory.size", 0x3f, ::MemOpReservedArg, Instr::MemorySize, Instr.MemorySize::class)
|
||||
opMapEntry("memory.grow", 0x40, ::MemOpReservedArg, Instr::MemoryGrow, Instr.MemoryGrow::class)
|
||||
|
||||
opMapEntry("i32.const", 0x41, ::ConstOpIntArg, Instr::I32Const, Instr.I32Const::class)
|
||||
opMapEntry("i64.const", 0x42, ::ConstOpLongArg, Instr::I64Const, Instr.I64Const::class)
|
||||
|
@ -47,10 +47,13 @@ open class AstToAsm {
|
||||
})
|
||||
// Now all import globals as getter (and maybe setter) method handles
|
||||
ctx.cls.fields.addAll(ctx.importGlobals.mapIndexed { index, import ->
|
||||
if ((import.kind as Node.Import.Kind.Global).type.mutable) throw CompileErr.MutableGlobalImport(index)
|
||||
FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, ctx.importGlobalGetterFieldName(index),
|
||||
val getter = FieldNode(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, ctx.importGlobalGetterFieldName(index),
|
||||
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
|
||||
ctx.cls.fields.addAll(ctx.mod.globals.mapIndexed { index, global ->
|
||||
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) =
|
||||
ctx.importFuncs.map { MethodHandle::class.ref } +
|
||||
// We know it's only getters
|
||||
ctx.importGlobals.map { MethodHandle::class.ref } +
|
||||
ctx.mod.imports.filter { it.kind is Node.Import.Kind.Table }.map { Array<MethodHandle>::class.ref }
|
||||
ctx.importGlobals.flatMap {
|
||||
// If it's mutable, it also comes with a setter
|
||||
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 ->
|
||||
// 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
|
||||
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 {
|
||||
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) =
|
||||
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(
|
||||
VarInsnNode(Opcodes.ALOAD, 0),
|
||||
VarInsnNode(Opcodes.ALOAD, ctx.importFuncs.size + importIndex + paramsBeforeImports + 1),
|
||||
VarInsnNode(Opcodes.ALOAD, importParamOffset + 1),
|
||||
FieldInsnNode(Opcodes.PUTFIELD, ctx.thisRef.asmName,
|
||||
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) =
|
||||
ctx.importFuncs.indices.fold(func) { func, importIndex ->
|
||||
@ -261,7 +285,10 @@ open class AstToAsm {
|
||||
|
||||
fun setConstructorTableImports(ctx: ClsContext, func: Func, paramsBeforeImports: Int) =
|
||||
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(
|
||||
VarInsnNode(Opcodes.ALOAD, 0),
|
||||
VarInsnNode(Opcodes.ALOAD, importIndex),
|
||||
@ -299,11 +326,14 @@ open class AstToAsm {
|
||||
global.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(
|
||||
VarInsnNode(
|
||||
Opcodes.ALOAD,
|
||||
ctx.importFuncs.size + it.index + paramsBeforeImports + 1
|
||||
),
|
||||
VarInsnNode(Opcodes.ALOAD, paramOffset),
|
||||
MethodInsnNode(
|
||||
Opcodes.INVOKEVIRTUAL,
|
||||
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
|
||||
// from the parameter
|
||||
// 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)).
|
||||
let { func -> addElemsToTable(ctx, func, paramsBeforeImports) }.
|
||||
// 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.Right -> global.v.type
|
||||
}
|
||||
if (type.mutable) throw CompileErr.MutableGlobalExport(export.index)
|
||||
// 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)
|
||||
method.addInsns(VarInsnNode(Opcodes.ALOAD, 0))
|
||||
if (global is Either.Left) method.addInsns(
|
||||
getter.addInsns(VarInsnNode(Opcodes.ALOAD, 0))
|
||||
if (global is Either.Left) getter.addInsns(
|
||||
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName,
|
||||
ctx.importGlobalGetterFieldName(export.index), MethodHandle::class.ref.asmDesc),
|
||||
MethodInsnNode(Opcodes.INVOKEVIRTUAL, MethodHandle::class.ref.asmName, "invokeExact",
|
||||
"()" + type.contentType.typeRef.asmDesc, false)
|
||||
) else method.addInsns(
|
||||
) else getter.addInsns(
|
||||
FieldInsnNode(Opcodes.GETFIELD, ctx.thisRef.asmName, ctx.globalName(export.index),
|
||||
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.I64 -> Opcodes.LRETURN
|
||||
Node.Type.Value.F32 -> Opcodes.FRETURN
|
||||
Node.Type.Value.F64 -> Opcodes.DRETURN
|
||||
}))
|
||||
method.visibleAnnotations = listOf(exportAnnotation(export))
|
||||
ctx.cls.methods.plusAssign(method)
|
||||
getter.visibleAnnotations = listOf(exportAnnotation(export))
|
||||
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) {
|
||||
|
@ -102,18 +102,6 @@ sealed class CompileErr(message: String, cause: Throwable? = null) : RuntimeExce
|
||||
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(
|
||||
val index: Int
|
||||
) : CompileErr("Expected init for global $index to be single constant value") {
|
||||
|
@ -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.I64Store32 ->
|
||||
applyStoreOp(ctx, fn, i as Node.Instr.Args.AlignOffset, index)
|
||||
is Node.Instr.CurrentMemory ->
|
||||
applyCurrentMemory(ctx, fn)
|
||||
is Node.Instr.GrowMemory ->
|
||||
applyGrowMemory(ctx, fn)
|
||||
is Node.Instr.MemorySize ->
|
||||
applyMemorySize(ctx, fn)
|
||||
is Node.Instr.MemoryGrow ->
|
||||
applyMemoryGrow(ctx, fn)
|
||||
is Node.Instr.I32Const ->
|
||||
fn.addInsns(i.value.const).push(Int::class.ref)
|
||||
is Node.Instr.I64Const ->
|
||||
@ -1062,14 +1062,14 @@ open class FuncBuilder {
|
||||
).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
|
||||
// the stack before this call. Result is an int.
|
||||
ctx.cls.assertHasMemory().let {
|
||||
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
|
||||
// stack since we need it
|
||||
ctx.cls.assertHasMemory().let {
|
||||
|
@ -194,7 +194,7 @@ open class InsnReworker {
|
||||
is Node.Instr.I64Store32 ->
|
||||
injectBeforeLastStackCount(Insn.MemNeededOnStack, 2)
|
||||
// Grow memory requires "mem" before the single param
|
||||
is Node.Instr.GrowMemory ->
|
||||
is Node.Instr.MemoryGrow ->
|
||||
injectBeforeLastStackCount(Insn.MemNeededOnStack, 1)
|
||||
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.I32Store8, is Node.Instr.I32Store16, is Node.Instr.I64Store8, is Node.Instr.I64Store16,
|
||||
is Node.Instr.I64Store32 -> POP_PARAM
|
||||
is Node.Instr.CurrentMemory -> PUSH_RESULT
|
||||
is Node.Instr.GrowMemory -> POP_PARAM + PUSH_RESULT
|
||||
is Node.Instr.MemorySize -> PUSH_RESULT
|
||||
is Node.Instr.MemoryGrow -> POP_PARAM + PUSH_RESULT
|
||||
is Node.Instr.I32Const, is Node.Instr.I64Const,
|
||||
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,
|
||||
@ -288,12 +288,12 @@ open class InsnReworker {
|
||||
val inc =
|
||||
if (lastCouldHaveMem) 0
|
||||
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
|
||||
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.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
|
||||
}
|
||||
(count + inc) to couldSetMemNext
|
||||
|
@ -144,8 +144,9 @@ open class BinaryToAst(
|
||||
}
|
||||
}
|
||||
|
||||
fun toLocals(b: ByteReader) = b.readVarUInt32AsInt().let { size ->
|
||||
toValueType(b).let { type -> List(size) { type } }
|
||||
fun toLocals(b: ByteReader): List<Node.Type.Value> {
|
||||
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))
|
||||
|
@ -1,12 +1,12 @@
|
||||
package asmble.io
|
||||
|
||||
import asmble.util.toIntExact
|
||||
import asmble.util.toUnsignedBigInt
|
||||
import asmble.util.toUnsignedLong
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.math.BigInteger
|
||||
|
||||
|
||||
abstract class ByteReader {
|
||||
abstract val isEof: Boolean
|
||||
|
||||
@ -34,27 +34,30 @@ abstract class ByteReader {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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 {
|
||||
require(it == 1 || it == 0)
|
||||
if (it != 1 && it != 0) throw IoErr.InvalidLeb128Number()
|
||||
it == 1
|
||||
}
|
||||
|
||||
fun readVarUInt7() = readUnsignedLeb128().let {
|
||||
require(it <= 255)
|
||||
if (it > 255) throw IoErr.InvalidLeb128Number()
|
||||
it.toShort()
|
||||
}
|
||||
|
||||
fun readVarUInt32() = readUnsignedLeb128().toUnsignedLong()
|
||||
|
||||
protected fun readUnsignedLeb128(): Int {
|
||||
protected fun readUnsignedLeb128(maxCount: Int = 4): Int {
|
||||
// Taken from Android source, Apache licensed
|
||||
var result = 0
|
||||
var cur: Int
|
||||
@ -63,12 +66,12 @@ abstract class ByteReader {
|
||||
cur = readByte().toInt() and 0xff
|
||||
result = result or ((cur and 0x7f) shl (count * 7))
|
||||
count++
|
||||
} while (cur and 0x80 == 0x80 && count < 5)
|
||||
if (cur and 0x80 == 0x80) throw NumberFormatException()
|
||||
} while (cur and 0x80 == 0x80 && count <= maxCount)
|
||||
if (cur and 0x80 == 0x80) throw IoErr.InvalidLeb128Number()
|
||||
return result
|
||||
}
|
||||
|
||||
private fun readSignedLeb128(): Long {
|
||||
private fun readSignedLeb128(maxCount: Int = 4): Long {
|
||||
// Taken from Android source, Apache licensed
|
||||
var result = 0L
|
||||
var cur: Int
|
||||
@ -79,8 +82,20 @@ abstract class ByteReader {
|
||||
result = result or ((cur and 0x7f).toLong() shl (count * 7))
|
||||
signBits = signBits shl 7
|
||||
count++
|
||||
} while (cur and 0x80 == 0x80 && count < 10)
|
||||
if (cur and 0x80 == 0x80) throw NumberFormatException()
|
||||
} while (cur and 0x80 == 0x80 && count <= maxCount)
|
||||
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
|
||||
return result
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package asmble.io
|
||||
|
||||
import asmble.AsmErr
|
||||
import java.math.BigInteger
|
||||
|
||||
sealed class IoErr(message: String, cause: Throwable? = null) : RuntimeException(message, cause), AsmErr {
|
||||
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") {
|
||||
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"
|
||||
}
|
||||
}
|
@ -116,9 +116,9 @@ interface Module {
|
||||
}
|
||||
|
||||
// Global imports
|
||||
val globalImports = mod.imports.mapNotNull {
|
||||
if (it.kind is Node.Import.Kind.Global) ctx.resolveImportGlobal(it, it.kind.type)
|
||||
else null
|
||||
val globalImports = mod.imports.flatMap {
|
||||
if (it.kind is Node.Import.Kind.Global) ctx.resolveImportGlobals(it, it.kind.type)
|
||||
else emptyList()
|
||||
}
|
||||
constructorParams += globalImports
|
||||
|
||||
|
@ -55,4 +55,12 @@ sealed class RunErr(message: String, cause: Throwable? = null) : RuntimeExceptio
|
||||
override val asmErrString get() = "unknown import"
|
||||
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"
|
||||
}
|
||||
}
|
@ -263,10 +263,12 @@ data class ScriptContext(
|
||||
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
|
||||
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) {
|
||||
is Node.Import.Kind.Func -> WasmExternalKind.FUNCTION
|
||||
is Node.Import.Kind.Table -> WasmExternalKind.TABLE
|
||||
@ -281,8 +283,18 @@ data class ScriptContext(
|
||||
bindImport(import, false,
|
||||
MethodType.methodType(funcType.ret?.jclass ?: Void.TYPE, funcType.params.map { it.jclass }))
|
||||
|
||||
fun resolveImportGlobal(import: Node.Import, globalType: Node.Type.Global) =
|
||||
bindImport(import, true, MethodType.methodType(globalType.contentType.jclass))
|
||||
fun resolveImportGlobals(import: Node.Import, globalType: Node.Type.Global): List<MethodHandle> {
|
||||
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) =
|
||||
bindImport(import, true, MethodType.methodType(Class.forName(mem.memType.asm.className))).
|
||||
|
@ -13,8 +13,8 @@ class SpecTestUnit(name: String, wast: String, expectedOutput: String?) : BaseTe
|
||||
override val shouldFail get() = name.endsWith(".fail")
|
||||
|
||||
override val defaultMaxMemPages get() = when (name) {
|
||||
"nop"-> 20
|
||||
"resizing" -> 830
|
||||
"nop" -> 20
|
||||
"memory_grow" -> 830
|
||||
"imports" -> 5
|
||||
else -> 1
|
||||
}
|
||||
|
Submodule compiler/src/test/resources/spec updated: 98b90e2ab2...b9cddea4dd
Reference in New Issue
Block a user