Bit more work supporting emscripten emulation

This commit is contained in:
Chad Retz 2017-04-21 15:00:27 -05:00
parent 5890a1cd7c
commit e2996212e9
12 changed files with 373 additions and 223 deletions

View File

@ -1,6 +1,7 @@
package asmble.cli
import asmble.compile.jvm.javaIdent
import asmble.run.jvm.Module
open class Invoke : ScriptCommand<Invoke.Args>() {
@ -37,7 +38,8 @@ open class Invoke : ScriptCommand<Invoke.Args>() {
// Instantiate the module
val module =
if (args.module == "<last-in-entry>") ctx.modules.lastOrNull() ?: error("No modules available")
else ctx.registrations[args.module] ?: error("Unable to find module registered as ${args.module}")
else ctx.registrations[args.module] as? Module.Instance ?:
error("Unable to find module registered as ${args.module}")
// Just make sure the module is instantiated here...
module.instance(ctx)
// If an export is provided, call it

View File

@ -2,6 +2,7 @@ package asmble.cli
import asmble.ast.Script
import asmble.compile.jvm.javaIdent
import asmble.run.jvm.Module
import asmble.run.jvm.ScriptContext
import java.io.File
import java.util.*
@ -70,9 +71,8 @@ abstract class ScriptCommand<T> : Command<T>() {
}
// Do registrations
ctx = args.registrations.fold(ctx) { ctx, (moduleName, className) ->
val cls = Class.forName(className, true, ctx.classLoader)
ctx.copy(registrations = ctx.registrations +
(moduleName to ScriptContext.NativeModule(cls, cls.newInstance())))
ctx.withModuleRegistered(moduleName,
Module.Native(Class.forName(className, true, ctx.classLoader).newInstance()))
}
if (args.specTestRegister) ctx = ctx.withHarnessRegistered()
return ctx

View File

@ -0,0 +1,151 @@
package asmble.run.jvm
import asmble.ast.Node
import asmble.compile.jvm.Mem
import asmble.compile.jvm.ref
import asmble.run.jvm.annotation.WasmName
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
import java.lang.reflect.Constructor
interface Module {
fun bindMethod(ctx: ScriptContext, wasmName: String, javaName: String, type: MethodType): MethodHandle?
data class Composite(val modules: List<Module>) : Module {
override fun bindMethod(ctx: ScriptContext, wasmName: String, javaName: String, type: MethodType) =
modules.asSequence().mapNotNull { it.bindMethod(ctx, wasmName, javaName, type) }.singleOrNull()
}
interface Instance : Module {
val cls: Class<*>
// Guaranteed to be the same instance when there is no error
fun instance(ctx: ScriptContext): Any
override fun bindMethod(ctx: ScriptContext, wasmName: String, javaName: String, type: MethodType) =
try {
MethodHandles.lookup().bind(instance(ctx), javaName, type)
} catch (_: NoSuchMethodException) {
// Try any method w/ the proper annotation
cls.methods.mapNotNull { method ->
if (method.getAnnotation(WasmName::class.java)?.value != wasmName) null
else MethodHandles.lookup().unreflect(method).bindTo(instance(ctx)).takeIf { it.type() == type }
}.singleOrNull()
}
}
data class Native(override val cls: Class<*>, val inst: Any) : Instance {
constructor(inst: Any) : this(inst::class.java, inst)
override fun instance(ctx: ScriptContext) = inst
}
class Compiled(
val mod: Node.Module,
override val cls: Class<*>,
val name: String?,
val mem: Mem
) : Instance {
private var inst: Any? = null
override fun instance(ctx: ScriptContext) =
synchronized(this) { inst ?: createInstance(ctx).also { inst = it } }
private fun createInstance(ctx: ScriptContext): Any {
// Find the constructor
var constructorParams = emptyList<Any>()
var constructor: Constructor<*>?
// If there is a memory import, we have to get the one with the mem class as the first
val memImport = mod.imports.find { it.kind is Node.Import.Kind.Memory }
val memLimit = if (memImport != null) {
constructor = cls.declaredConstructors.find { it.parameterTypes.firstOrNull()?.ref == mem.memType }
val memImportKind = memImport.kind as Node.Import.Kind.Memory
val memInst = ctx.resolveImportMemory(memImport, memImportKind.type, mem)
constructorParams += memInst
val (memLimit, memCap) = mem.limitAndCapacity(memInst)
if (memLimit < memImportKind.type.limits.initial * Mem.PAGE_SIZE)
throw RunErr.ImportMemoryLimitTooSmall(memImportKind.type.limits.initial * Mem.PAGE_SIZE, memLimit)
memImportKind.type.limits.maximum?.let {
if (memCap > it * Mem.PAGE_SIZE)
throw RunErr.ImportMemoryCapacityTooLarge(it * Mem.PAGE_SIZE, memCap)
}
memLimit
} else {
// Find the constructor with no max mem amount (i.e. not int and not memory)
constructor = cls.declaredConstructors.find {
val memClass = Class.forName(mem.memType.asm.className)
when (it.parameterTypes.firstOrNull()) {
Int::class.java, memClass -> false
else -> true
}
}
// If it is not there, find the one w/ the max mem amount
val maybeMem = mod.memories.firstOrNull()
if (constructor == null) {
val maxMem = Math.max(maybeMem?.limits?.initial ?: 0, ctx.defaultMaxMemPages)
constructor = cls.declaredConstructors.find { it.parameterTypes.firstOrNull() == Int::class.java }
constructorParams += maxMem * Mem.PAGE_SIZE
}
maybeMem?.limits?.initial?.let { it * Mem.PAGE_SIZE }
}
if (constructor == null) error("Unable to find suitable module constructor")
// Function imports
constructorParams += mod.imports.mapNotNull {
if (it.kind is Node.Import.Kind.Func) ctx.resolveImportFunc(it, mod.types[it.kind.typeIndex])
else null
}
// Global imports
val globalImports = mod.imports.mapNotNull {
if (it.kind is Node.Import.Kind.Global) ctx.resolveImportGlobal(it, it.kind.type)
else null
}
constructorParams += globalImports
// Table imports
val tableImport = mod.imports.find { it.kind is Node.Import.Kind.Table }
val tableSize = if (tableImport != null) {
val tableImportKind = tableImport.kind as Node.Import.Kind.Table
val table = ctx.resolveImportTable(tableImport, tableImportKind.type)
if (table.size < tableImportKind.type.limits.initial)
throw RunErr.ImportTableTooSmall(tableImportKind.type.limits.initial, table.size)
tableImportKind.type.limits.maximum?.let {
if (table.size > it) throw RunErr.ImportTableTooLarge(it, table.size)
}
constructorParams = constructorParams.plusElement(table)
table.size
} else mod.tables.firstOrNull()?.limits?.initial
// We need to validate that elems can fit in table and data can fit in mem
fun constIntExpr(insns: List<Node.Instr>): Int? = insns.singleOrNull()?.let {
when (it) {
is Node.Instr.I32Const -> it.value
is Node.Instr.GetGlobal ->
if (it.index < globalImports.size) {
// Imports we already have
if (globalImports[it.index].type().returnType() == Int::class.java) {
globalImports[it.index].invokeWithArguments() as Int
} else null
} else constIntExpr(mod.globals[it.index - globalImports.size].init)
else -> null
}
}
if (tableSize != null) mod.elems.forEach { elem ->
constIntExpr(elem.offset)?.let { offset ->
if (offset >= tableSize) throw RunErr.InvalidElemIndex(offset, tableSize)
}
}
if (memLimit != null) mod.data.forEach { data ->
constIntExpr(data.offset)?.let { offset ->
if (offset < 0 || offset + data.data.size > memLimit)
throw RunErr.InvalidDataIndex(offset, data.data.size, memLimit)
}
}
// Construct
ctx.debug { "Instantiating $cls using $constructor with params $constructorParams" }
return constructor.newInstance(*constructorParams.toTypedArray())
}
}
}

View File

@ -5,26 +5,22 @@ import asmble.ast.Script
import asmble.compile.jvm.*
import asmble.io.AstToSExpr
import asmble.io.SExprToStr
import asmble.run.jvm.annotation.WasmName
import asmble.run.jvm.emscripten.Env
import asmble.util.Logger
import asmble.util.toRawIntBits
import asmble.util.toRawLongBits
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.Opcodes
import java.io.OutputStream
import java.io.PrintWriter
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
import java.lang.reflect.Constructor
import java.lang.reflect.InvocationTargetException
import java.util.*
data class ScriptContext(
val packageName: String,
val modules: List<CompiledModule> = emptyList(),
val modules: List<Module.Compiled> = emptyList(),
val registrations: Map<String, Module> = emptyMap(),
val logger: Logger = Logger.Print(Logger.Level.OFF),
val adjustContext: (ClsContext) -> ClsContext = { it },
@ -34,12 +30,9 @@ data class ScriptContext(
val defaultMaxMemPages: Int = 1
) : Logger by logger {
fun withHarnessRegistered(out: PrintWriter = PrintWriter(System.out, true)) =
copy(registrations = registrations + (
"spectest" to NativeModule(TestHarness::class.java, TestHarness(out))
))
withModuleRegistered("spectest", Module.Native(TestHarness(out)))
fun withEmscriptenEnvRegistered(out: OutputStream = System.out) =
copy(registrations = registrations + ("env" to NativeModule(Env::class.java, Env(logger, out))))
fun withModuleRegistered(name: String, mod: Module) = copy(registrations = registrations + (name to mod))
fun runCommand(cmd: Script.Cmd) = when (cmd) {
is Script.Cmd.Module ->
@ -251,7 +244,7 @@ data class ScriptContext(
fun withCompiledModule(mod: Node.Module, className: String, name: String?) =
copy(modules = modules + compileModule(mod, className, name))
fun compileModule(mod: Node.Module, className: String, name: String?): CompiledModule {
fun compileModule(mod: Node.Module, className: String, name: String?): Module.Compiled {
val ctx = ClsContext(
packageName = packageName,
className = className,
@ -259,28 +252,15 @@ data class ScriptContext(
logger = logger
).let(adjustContext)
AstToAsm.fromModule(ctx)
return CompiledModule(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 {
// Find a method that matches our expectations
val module = registrations[import.module] ?: error("Unable to find module ${import.module}")
// TODO: do I want to introduce a complicated set of code that will find
// a method that can accept the given params including varargs, boxing, etc?
// I doubt it since it's only the JVM layer, WASM doesn't have parametric polymorphism
try {
val javaName = if (getter) "get" + import.field.javaIdent.capitalize() else import.field.javaIdent
return MethodHandles.lookup().bind(module.instance(this), javaName, methodType)
} catch (e: NoSuchMethodException) {
// Try any method w/ the proper annotation
module.cls.methods.forEach { method ->
if (method.getAnnotation(WasmName::class.java)?.value == import.field) {
val handle = MethodHandles.lookup().unreflect(method).bindTo(module.instance(this))
if (handle.type() == methodType) return handle
}
}
throw e
}
val javaName = if (getter) "get" + import.field.javaIdent.capitalize() else import.field.javaIdent
return module.bindMethod(this, import.field, javaName, methodType) ?:
throw NoSuchMethodException("Cannot find import for ${import.module}::${import.field}")
}
fun resolveImportFunc(import: Node.Import, funcType: Node.Type.Func) =
@ -298,125 +278,6 @@ data class ScriptContext(
bindImport(import, true, MethodType.methodType(Array<MethodHandle>::class.java)).
invokeWithArguments()!! as Array<MethodHandle>
interface Module {
val cls: Class<*>
// Guaranteed to be the same instance when there is no error
fun instance(ctx: ScriptContext): Any
}
class NativeModule(override val cls: Class<*>, val inst: Any) : Module {
override fun instance(ctx: ScriptContext) = inst
}
class CompiledModule(
val mod: Node.Module,
override val cls: Class<*>,
val name: String?,
val mem: Mem
) : Module {
private var inst: Any? = null
override fun instance(ctx: ScriptContext) =
synchronized(this) { inst ?: createInstance(ctx).also { inst = it } }
private fun createInstance(ctx: ScriptContext): Any {
// Find the constructor
var constructorParams = emptyList<Any>()
var constructor: Constructor<*>?
// If there is a memory import, we have to get the one with the mem class as the first
val memImport = mod.imports.find { it.kind is Node.Import.Kind.Memory }
val memLimit = if (memImport != null) {
constructor = cls.declaredConstructors.find { it.parameterTypes.firstOrNull()?.ref == mem.memType }
val memImportKind = memImport.kind as Node.Import.Kind.Memory
val memInst = ctx.resolveImportMemory(memImport, memImportKind.type, mem)
constructorParams += memInst
val (memLimit, memCap) = mem.limitAndCapacity(memInst)
if (memLimit < memImportKind.type.limits.initial * Mem.PAGE_SIZE)
throw RunErr.ImportMemoryLimitTooSmall(memImportKind.type.limits.initial * Mem.PAGE_SIZE, memLimit)
memImportKind.type.limits.maximum?.let {
if (memCap > it * Mem.PAGE_SIZE)
throw RunErr.ImportMemoryCapacityTooLarge(it * Mem.PAGE_SIZE, memCap)
}
memLimit
} else {
// Find the constructor with no max mem amount (i.e. not int and not memory)
constructor = cls.declaredConstructors.find {
val memClass = Class.forName(mem.memType.asm.className)
when (it.parameterTypes.firstOrNull()) {
Int::class.java, memClass -> false
else -> true
}
}
// If it is not there, find the one w/ the max mem amount
val maybeMem = mod.memories.firstOrNull()
if (constructor == null) {
val maxMem = Math.max(maybeMem?.limits?.initial ?: 0, ctx.defaultMaxMemPages)
constructor = cls.declaredConstructors.find { it.parameterTypes.firstOrNull() == Int::class.java }
constructorParams += maxMem * Mem.PAGE_SIZE
}
maybeMem?.limits?.initial?.let { it * Mem.PAGE_SIZE }
}
if (constructor == null) error("Unable to find suitable module constructor")
// Function imports
constructorParams += mod.imports.mapNotNull {
if (it.kind is Node.Import.Kind.Func) ctx.resolveImportFunc(it, mod.types[it.kind.typeIndex])
else null
}
// Global imports
val globalImports = mod.imports.mapNotNull {
if (it.kind is Node.Import.Kind.Global) ctx.resolveImportGlobal(it, it.kind.type)
else null
}
constructorParams += globalImports
// Table imports
val tableImport = mod.imports.find { it.kind is Node.Import.Kind.Table }
val tableSize = if (tableImport != null) {
val tableImportKind = tableImport.kind as Node.Import.Kind.Table
val table = ctx.resolveImportTable(tableImport, tableImportKind.type)
if (table.size < tableImportKind.type.limits.initial)
throw RunErr.ImportTableTooSmall(tableImportKind.type.limits.initial, table.size)
tableImportKind.type.limits.maximum?.let {
if (table.size > it) throw RunErr.ImportTableTooLarge(it, table.size)
}
constructorParams = constructorParams.plusElement(table)
table.size
} else mod.tables.firstOrNull()?.limits?.initial
// We need to validate that elems can fit in table and data can fit in mem
fun constIntExpr(insns: List<Node.Instr>): Int? = insns.singleOrNull()?.let {
when (it) {
is Node.Instr.I32Const -> it.value
is Node.Instr.GetGlobal ->
if (it.index < globalImports.size) {
// Imports we already have
if (globalImports[it.index].type().returnType() == Int::class.java) {
globalImports[it.index].invokeWithArguments() as Int
} else null
} else constIntExpr(mod.globals[it.index - globalImports.size].init)
else -> null
}
}
if (tableSize != null) mod.elems.forEach { elem ->
constIntExpr(elem.offset)?.let { offset ->
if (offset >= tableSize) throw RunErr.InvalidElemIndex(offset, tableSize)
}
}
if (memLimit != null) mod.data.forEach { data ->
constIntExpr(data.offset)?.let { offset ->
if (offset < 0 || offset + data.data.size > memLimit)
throw RunErr.InvalidDataIndex(offset, data.data.size, memLimit)
}
}
// Construct
ctx.debug { "Instantiating $cls using $constructor with params $constructorParams" }
return constructor.newInstance(*constructorParams.toTypedArray())
}
}
open class SimpleClassLoader(parent: ClassLoader, logger: Logger) : ClassLoader(parent), Logger by logger {
fun fromBuiltContext(ctx: ClsContext): Class<*> {
trace { "Computing frames for ASM class:\n" + ctx.cls.toAsmString() }

View File

@ -1,63 +1,44 @@
package asmble.run.jvm.emscripten
import asmble.compile.jvm.Mem
import asmble.run.jvm.Module
import asmble.run.jvm.annotation.WasmName
import asmble.util.Logger
import asmble.util.get
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
class Env(
val logger: Logger,
val staticBump: Int,
val out: OutputStream
) : Logger by logger {
fun alignTo16(num: Int) = Math.ceil(num / 16.0).toInt() * 16
val memory = ByteBuffer.allocateDirect(256 * Mem.PAGE_SIZE).order(ByteOrder.LITTLE_ENDIAN)
var fds: Map<Int, Stream> = mapOf(
1 to Stream.OutputStream(out)
)
init {
// Emscripten sets where "stack top" can start in mem at position 1024.
// TODO: Waiting for https://github.com/WebAssembly/binaryen/issues/979
val staticBump = 4044
// See https://github.com/WebAssembly/binaryen/issues/979
val stackBase = alignTo16(staticBump + 1024 + 16)
val stackTop = stackBase + TOTAL_STACK
// We have to set some values like Emscripten
memory.putInt(1024, stackTop)
}
fun abort() { TODO() }
@WasmName("__syscall6")
fun close(arg0: Int, arg1: Int): Int { TODO() }
@WasmName("__syscall54")
fun ioctl(which: Int, varargs: Int): Int {
val fd = fd(memory.getInt(varargs))
val op = memory.getInt(varargs + 4)
return when (IoctlOp[op]) {
IoctlOp.TCGETS, IoctlOp.TCSETS, IoctlOp.TIOCGWINSZ ->
if (fd.tty == null) -Errno.ENOTTY.number else 0
IoctlOp.TIOCGPGRP ->
if (fd.tty == null) -Errno.ENOTTY.number else {
memory.putInt(memory.getInt(varargs + 8), 0)
0
}
IoctlOp.TIOCSPGRP ->
if (fd.tty == null) -Errno.ENOTTY.number else -Errno.EINVAL.number
IoctlOp.FIONREAD ->
if (fd.tty == null) -Errno.ENOTTY.number else TODO("ioctl FIONREAD")
null ->
error("Unrecognized op: $op")
internal fun readCStringBytes(ptr: Int) = ByteArrayOutputStream().let { bos ->
var ptr = ptr
while (true) {
val byte = memory.get(ptr++)
if (byte == 0.toByte()) break
bos.write(byte.toInt())
}
bos.toByteArray()
}
@WasmName("__syscall140")
fun llseek(arg0: Int, arg1: Int): Int { TODO() }
internal fun readCString(ptr: Int) = readCStringBytes(ptr).toString(Charsets.ISO_8859_1)
fun abort() { TODO() }
@WasmName("__lock")
fun lock(arg: Int) { TODO() }
@ -67,45 +48,20 @@ class Env(
@WasmName("__unlock")
fun unlock(arg: Int) { TODO() }
@WasmName("__syscall146")
fun writev(which: Int, varargs: Int): Int {
val fd = fd(memory.getInt(varargs))
val iov = memory.getInt(varargs + 4)
val iovcnt = memory.getInt(varargs + 8)
return (0 until iovcnt).fold(0) { total, i ->
val ptr = memory.getInt(iov + (i * 8))
val len = memory.getInt(iov + (i * 8) + 4)
if (len > 0) {
fd.write(try {
ByteArray(len).also { memory.get(ptr, it) }
} catch (e: Exception) {
// TODO: set errno?
return -1
})
}
total + len
}
}
private fun fd(v: Int) = fds[v] ?: Errno.EBADF.raise()
enum class IoctlOp(val number: Int) {
TCGETS(0x5401),
TCSETS(0x5402),
TIOCGPGRP(0x540F),
TIOCSPGRP(0x5410),
FIONREAD(0x541B),
TIOCGWINSZ(0x5413);
companion object {
val byNumber = IoctlOp.values().associateBy { it.number }
operator fun get(number: Int) = byNumber[number]
}
}
companion object {
const val TOTAL_STACK = 5242880
const val TOTAL_MEMORY = 16777216
const val GLOBAL_BASE = 1024
val subModules = listOf(::Stdio, ::Syscall)
fun module(
logger: Logger,
staticBump: Int,
out: OutputStream
): Module = Env(logger, staticBump, out).let { env ->
Module.Composite(subModules.fold(listOf(Module.Native(env))) { list, subMod ->
list + Module.Native(subMod(env))
})
}
}
}

View File

@ -0,0 +1,28 @@
package asmble.run.jvm.emscripten
class Stdio(val env: Env) {
fun printf(format: Int, argStart: Int) = format(format, argStart).let { formatted ->
env.out.write(formatted.toByteArray(Charsets.ISO_8859_1))
formatted.length
}
private fun format(format: Int, argStart: Int): String {
// TODO: the rest of this. We should actually take musl, compile it to the JVM,
// and then go from there. Not musl.wast which has some imports of its own.
val str = env.readCString(format)
// Only support %s for now...
val strReplacementIndices = str.foldIndexed(emptyList<Int>()) { index, indices, char ->
if (char != '%') indices
else if (str.getOrNull(index + 1) != 's') error("Only '%s' supported for now")
else indices + index
}
val strs = strReplacementIndices.indices.map { index ->
env.readCString(env.memory.getInt(argStart + (index * 4)))
}
// Replace reversed
return strReplacementIndices.zip(strs).asReversed().fold(str) { str, (index, toPlace) ->
str.substring(0, index) + toPlace + str.substring(index + 2)
}
}
}

View File

@ -0,0 +1,73 @@
package asmble.run.jvm.emscripten
import asmble.run.jvm.annotation.WasmName
import asmble.util.get
class Syscall(val env: Env) {
var fds: Map<Int, Stream> = mapOf(
1 to Stream.OutputStream(env.out)
)
@WasmName("__syscall6")
fun close(arg0: Int, arg1: Int): Int { TODO() }
@WasmName("__syscall54")
fun ioctl(which: Int, varargs: Int): Int {
val fd = fd(env.memory.getInt(varargs))
val op = env.memory.getInt(varargs + 4)
return when (IoctlOp[op]) {
IoctlOp.TCGETS, IoctlOp.TCSETS, IoctlOp.TIOCGWINSZ ->
if (fd.tty == null) -Errno.ENOTTY.number else 0
IoctlOp.TIOCGPGRP ->
if (fd.tty == null) -Errno.ENOTTY.number else {
env.memory.putInt(env.memory.getInt(varargs + 8), 0)
0
}
IoctlOp.TIOCSPGRP ->
if (fd.tty == null) -Errno.ENOTTY.number else -Errno.EINVAL.number
IoctlOp.FIONREAD ->
if (fd.tty == null) -Errno.ENOTTY.number else TODO("ioctl FIONREAD")
null ->
error("Unrecognized op: $op")
}
}
@WasmName("__syscall140")
fun llseek(arg0: Int, arg1: Int): Int { TODO() }
@WasmName("__syscall146")
fun writev(which: Int, varargs: Int): Int {
val fd = fd(env.memory.getInt(varargs))
val iov = env.memory.getInt(varargs + 4)
val iovcnt = env.memory.getInt(varargs + 8)
return (0 until iovcnt).fold(0) { total, i ->
val ptr = env.memory.getInt(iov + (i * 8))
val len = env.memory.getInt(iov + (i * 8) + 4)
if (len > 0) {
fd.write(try {
ByteArray(len).also { env.memory.get(ptr, it) }
} catch (e: Exception) {
// TODO: set errno?
return -1
})
}
total + len
}
}
private fun fd(v: Int) = fds[v] ?: Errno.EBADF.raise()
enum class IoctlOp(val number: Int) {
TCGETS(0x5401),
TCSETS(0x5402),
TIOCGPGRP(0x540F),
TIOCSPGRP(0x5410),
FIONREAD(0x541B),
TIOCGWINSZ(0x5413);
companion object {
val byNumber = IoctlOp.values().associateBy { it.number }
operator fun get(number: Int) = byNumber[number]
}
}
}

View File

@ -26,6 +26,19 @@ class SpecTestUnit(val name: String, val wast: String, val expectedOutput: Strin
else -> 1
}
val emscriptenStaticBump by lazy {
// I am not about to pull in a JSON parser just for this
wast.lastIndexOf(";; METADATA:").takeIf { it != -1 }?.let { metaIndex ->
wast.indexOfAny(listOf("\n", "\"staticBump\": "), metaIndex).
takeIf { it != -1 && wast[it] != '\n' }?.
let { bumpIndex ->
wast.indexOfAny(charArrayOf('\n', ','), bumpIndex).takeIf { it != -1 }?.let { commaIndex ->
wast.substring(bumpIndex + 14, commaIndex).trim().toIntOrNull()
}
}
}
}
fun warningInsteadOfErrReason(t: Throwable) = when (name) {
// NaN bit patterns can be off
"float_literals", "float_exprs" ->

View File

@ -3,6 +3,7 @@ package asmble.run.jvm
import asmble.SpecTestUnit
import asmble.io.AstToSExpr
import asmble.io.SExprToStr
import asmble.run.jvm.emscripten.Env
import asmble.util.Logger
import org.junit.Assume
import org.junit.Test
@ -40,8 +41,12 @@ class RunTest(val unit: SpecTestUnit) : Logger by Logger.Print(Logger.Level.INFO
logger = this,
adjustContext = { it.copy(eagerFailLargeMemOffset = false) },
defaultMaxMemPages = unit.defaultMaxMemPages
).withHarnessRegistered(PrintWriter(OutputStreamWriter(out, Charsets.UTF_8), true)).
withEmscriptenEnvRegistered(out)
).withHarnessRegistered(PrintWriter(OutputStreamWriter(out, Charsets.UTF_8), true))
// If there's a staticBump, we are an emscripten mod and we need to include the env
unit.emscriptenStaticBump?.also { staticBump ->
scriptContext = scriptContext.withModuleRegistered("env", Env.module(this, staticBump, out))
}
// This will fail assertions as necessary
scriptContext = unit.script.commands.fold(scriptContext) { scriptContext, cmd ->

View File

@ -0,0 +1 @@
Hello, world!

View File

@ -0,0 +1,4 @@
#include <stdio.h>
int main(int argc, char ** argv) {
printf("%s, %s!\n", "Hello", "world");
}

View File

@ -0,0 +1,56 @@
(module
(type $FUNCSIG$ii (func (param i32) (result i32)))
(type $FUNCSIG$iii (func (param i32 i32) (result i32)))
(import "env" "printf" (func $printf (param i32 i32) (result i32)))
(import "env" "memory" (memory $0 256))
(table 0 anyfunc)
(data (i32.const 1040) "%s, %s!\n\00")
(data (i32.const 1056) "Hello\00")
(data (i32.const 1072) "world\00")
(export "main" (func $main))
(func $main (param $0 i32) (param $1 i32) (result i32)
(local $2 i32)
(i32.store offset=1024
(i32.const 0)
(tee_local $2
(i32.sub
(i32.load offset=1024
(i32.const 0)
)
(i32.const 16)
)
)
)
(i32.store offset=12
(get_local $2)
(get_local $0)
)
(i32.store offset=8
(get_local $2)
(get_local $1)
)
(i32.store offset=4
(get_local $2)
(i32.const 1072)
)
(i32.store
(get_local $2)
(i32.const 1056)
)
(drop
(call $printf
(i32.const 1040)
(get_local $2)
)
)
(i32.store offset=1024
(i32.const 0)
(i32.add
(get_local $2)
(i32.const 16)
)
)
(i32.const 0)
)
)
;; METADATA: { "asmConsts": {},"staticBump": 54, "initializers": [] }