mirror of
https://github.com/fluencelabs/asmble
synced 2025-07-04 08:51:35 +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
|
||||||
|
|
||||||
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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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") {
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
@ -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))).
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
Submodule compiler/src/test/resources/spec updated: 98b90e2ab2...b9cddea4dd
Reference in New Issue
Block a user