mirror of
https://github.com/fluencelabs/asmble
synced 2025-04-24 22:32:19 +00:00
Bit more work supporting emscripten emulation
This commit is contained in:
parent
5890a1cd7c
commit
e2996212e9
@ -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
|
||||
|
@ -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
|
||||
|
151
src/main/kotlin/asmble/run/jvm/Module.kt
Normal file
151
src/main/kotlin/asmble/run/jvm/Module.kt
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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() }
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
28
src/main/kotlin/asmble/run/jvm/emscripten/Stdio.kt
Normal file
28
src/main/kotlin/asmble/run/jvm/emscripten/Stdio.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
73
src/main/kotlin/asmble/run/jvm/emscripten/Syscall.kt
Normal file
73
src/main/kotlin/asmble/run/jvm/emscripten/Syscall.kt
Normal 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]
|
||||
}
|
||||
}
|
||||
}
|
@ -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" ->
|
||||
|
@ -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 ->
|
||||
|
@ -0,0 +1 @@
|
||||
Hello, world!
|
4
src/test/resources/local-spec/hello-side-module.c
Normal file
4
src/test/resources/local-spec/hello-side-module.c
Normal file
@ -0,0 +1,4 @@
|
||||
#include <stdio.h>
|
||||
int main(int argc, char ** argv) {
|
||||
printf("%s, %s!\n", "Hello", "world");
|
||||
}
|
56
src/test/resources/local-spec/hello-side-module.wast
Normal file
56
src/test/resources/local-spec/hello-side-module.wast
Normal 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": [] }
|
Loading…
x
Reference in New Issue
Block a user