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
|
package asmble.cli
|
||||||
|
|
||||||
import asmble.compile.jvm.javaIdent
|
import asmble.compile.jvm.javaIdent
|
||||||
|
import asmble.run.jvm.Module
|
||||||
|
|
||||||
open class Invoke : ScriptCommand<Invoke.Args>() {
|
open class Invoke : ScriptCommand<Invoke.Args>() {
|
||||||
|
|
||||||
@ -37,7 +38,8 @@ open class Invoke : ScriptCommand<Invoke.Args>() {
|
|||||||
// Instantiate the module
|
// Instantiate the module
|
||||||
val module =
|
val module =
|
||||||
if (args.module == "<last-in-entry>") ctx.modules.lastOrNull() ?: error("No modules available")
|
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...
|
// Just make sure the module is instantiated here...
|
||||||
module.instance(ctx)
|
module.instance(ctx)
|
||||||
// If an export is provided, call it
|
// If an export is provided, call it
|
||||||
|
@ -2,6 +2,7 @@ package asmble.cli
|
|||||||
|
|
||||||
import asmble.ast.Script
|
import asmble.ast.Script
|
||||||
import asmble.compile.jvm.javaIdent
|
import asmble.compile.jvm.javaIdent
|
||||||
|
import asmble.run.jvm.Module
|
||||||
import asmble.run.jvm.ScriptContext
|
import asmble.run.jvm.ScriptContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -70,9 +71,8 @@ abstract class ScriptCommand<T> : Command<T>() {
|
|||||||
}
|
}
|
||||||
// Do registrations
|
// Do registrations
|
||||||
ctx = args.registrations.fold(ctx) { ctx, (moduleName, className) ->
|
ctx = args.registrations.fold(ctx) { ctx, (moduleName, className) ->
|
||||||
val cls = Class.forName(className, true, ctx.classLoader)
|
ctx.withModuleRegistered(moduleName,
|
||||||
ctx.copy(registrations = ctx.registrations +
|
Module.Native(Class.forName(className, true, ctx.classLoader).newInstance()))
|
||||||
(moduleName to ScriptContext.NativeModule(cls, cls.newInstance())))
|
|
||||||
}
|
}
|
||||||
if (args.specTestRegister) ctx = ctx.withHarnessRegistered()
|
if (args.specTestRegister) ctx = ctx.withHarnessRegistered()
|
||||||
return ctx
|
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.compile.jvm.*
|
||||||
import asmble.io.AstToSExpr
|
import asmble.io.AstToSExpr
|
||||||
import asmble.io.SExprToStr
|
import asmble.io.SExprToStr
|
||||||
import asmble.run.jvm.annotation.WasmName
|
|
||||||
import asmble.run.jvm.emscripten.Env
|
|
||||||
import asmble.util.Logger
|
import asmble.util.Logger
|
||||||
import asmble.util.toRawIntBits
|
import asmble.util.toRawIntBits
|
||||||
import asmble.util.toRawLongBits
|
import asmble.util.toRawLongBits
|
||||||
import org.objectweb.asm.ClassReader
|
import org.objectweb.asm.ClassReader
|
||||||
import org.objectweb.asm.ClassVisitor
|
import org.objectweb.asm.ClassVisitor
|
||||||
import org.objectweb.asm.Opcodes
|
import org.objectweb.asm.Opcodes
|
||||||
import java.io.OutputStream
|
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.lang.invoke.MethodHandle
|
import java.lang.invoke.MethodHandle
|
||||||
import java.lang.invoke.MethodHandles
|
import java.lang.invoke.MethodHandles
|
||||||
import java.lang.invoke.MethodType
|
import java.lang.invoke.MethodType
|
||||||
import java.lang.reflect.Constructor
|
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
data class ScriptContext(
|
data class ScriptContext(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val modules: List<CompiledModule> = emptyList(),
|
val modules: List<Module.Compiled> = emptyList(),
|
||||||
val registrations: Map<String, Module> = emptyMap(),
|
val registrations: Map<String, Module> = emptyMap(),
|
||||||
val logger: Logger = Logger.Print(Logger.Level.OFF),
|
val logger: Logger = Logger.Print(Logger.Level.OFF),
|
||||||
val adjustContext: (ClsContext) -> ClsContext = { it },
|
val adjustContext: (ClsContext) -> ClsContext = { it },
|
||||||
@ -34,12 +30,9 @@ data class ScriptContext(
|
|||||||
val defaultMaxMemPages: Int = 1
|
val defaultMaxMemPages: Int = 1
|
||||||
) : Logger by logger {
|
) : Logger by logger {
|
||||||
fun withHarnessRegistered(out: PrintWriter = PrintWriter(System.out, true)) =
|
fun withHarnessRegistered(out: PrintWriter = PrintWriter(System.out, true)) =
|
||||||
copy(registrations = registrations + (
|
withModuleRegistered("spectest", Module.Native(TestHarness(out)))
|
||||||
"spectest" to NativeModule(TestHarness::class.java, TestHarness(out))
|
|
||||||
))
|
|
||||||
|
|
||||||
fun withEmscriptenEnvRegistered(out: OutputStream = System.out) =
|
fun withModuleRegistered(name: String, mod: Module) = copy(registrations = registrations + (name to mod))
|
||||||
copy(registrations = registrations + ("env" to NativeModule(Env::class.java, Env(logger, out))))
|
|
||||||
|
|
||||||
fun runCommand(cmd: Script.Cmd) = when (cmd) {
|
fun runCommand(cmd: Script.Cmd) = when (cmd) {
|
||||||
is Script.Cmd.Module ->
|
is Script.Cmd.Module ->
|
||||||
@ -251,7 +244,7 @@ data class ScriptContext(
|
|||||||
fun withCompiledModule(mod: Node.Module, className: String, name: String?) =
|
fun withCompiledModule(mod: Node.Module, className: String, name: String?) =
|
||||||
copy(modules = modules + compileModule(mod, className, name))
|
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(
|
val ctx = ClsContext(
|
||||||
packageName = packageName,
|
packageName = packageName,
|
||||||
className = className,
|
className = className,
|
||||||
@ -259,28 +252,15 @@ data class ScriptContext(
|
|||||||
logger = logger
|
logger = logger
|
||||||
).let(adjustContext)
|
).let(adjustContext)
|
||||||
AstToAsm.fromModule(ctx)
|
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 {
|
fun bindImport(import: Node.Import, getter: Boolean, methodType: MethodType): MethodHandle {
|
||||||
// Find a method that matches our expectations
|
// Find a method that matches our expectations
|
||||||
val module = registrations[import.module] ?: error("Unable to find module ${import.module}")
|
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
|
val javaName = if (getter) "get" + import.field.javaIdent.capitalize() else import.field.javaIdent
|
||||||
// a method that can accept the given params including varargs, boxing, etc?
|
return module.bindMethod(this, import.field, javaName, methodType) ?:
|
||||||
// I doubt it since it's only the JVM layer, WASM doesn't have parametric polymorphism
|
throw NoSuchMethodException("Cannot find import for ${import.module}::${import.field}")
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resolveImportFunc(import: Node.Import, funcType: Node.Type.Func) =
|
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)).
|
bindImport(import, true, MethodType.methodType(Array<MethodHandle>::class.java)).
|
||||||
invokeWithArguments()!! as Array<MethodHandle>
|
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 {
|
open class SimpleClassLoader(parent: ClassLoader, logger: Logger) : ClassLoader(parent), Logger by logger {
|
||||||
fun fromBuiltContext(ctx: ClsContext): Class<*> {
|
fun fromBuiltContext(ctx: ClsContext): Class<*> {
|
||||||
trace { "Computing frames for ASM class:\n" + ctx.cls.toAsmString() }
|
trace { "Computing frames for ASM class:\n" + ctx.cls.toAsmString() }
|
||||||
|
@ -1,63 +1,44 @@
|
|||||||
package asmble.run.jvm.emscripten
|
package asmble.run.jvm.emscripten
|
||||||
|
|
||||||
import asmble.compile.jvm.Mem
|
import asmble.compile.jvm.Mem
|
||||||
|
import asmble.run.jvm.Module
|
||||||
import asmble.run.jvm.annotation.WasmName
|
import asmble.run.jvm.annotation.WasmName
|
||||||
import asmble.util.Logger
|
import asmble.util.Logger
|
||||||
import asmble.util.get
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
|
|
||||||
class Env(
|
class Env(
|
||||||
val logger: Logger,
|
val logger: Logger,
|
||||||
|
val staticBump: Int,
|
||||||
val out: OutputStream
|
val out: OutputStream
|
||||||
) : Logger by logger {
|
) : Logger by logger {
|
||||||
fun alignTo16(num: Int) = Math.ceil(num / 16.0).toInt() * 16
|
fun alignTo16(num: Int) = Math.ceil(num / 16.0).toInt() * 16
|
||||||
|
|
||||||
val memory = ByteBuffer.allocateDirect(256 * Mem.PAGE_SIZE).order(ByteOrder.LITTLE_ENDIAN)
|
val memory = ByteBuffer.allocateDirect(256 * Mem.PAGE_SIZE).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
|
||||||
var fds: Map<Int, Stream> = mapOf(
|
|
||||||
1 to Stream.OutputStream(out)
|
|
||||||
)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Emscripten sets where "stack top" can start in mem at position 1024.
|
// Emscripten sets where "stack top" can start in mem at position 1024.
|
||||||
// TODO: Waiting for https://github.com/WebAssembly/binaryen/issues/979
|
// See https://github.com/WebAssembly/binaryen/issues/979
|
||||||
val staticBump = 4044
|
|
||||||
val stackBase = alignTo16(staticBump + 1024 + 16)
|
val stackBase = alignTo16(staticBump + 1024 + 16)
|
||||||
val stackTop = stackBase + TOTAL_STACK
|
val stackTop = stackBase + TOTAL_STACK
|
||||||
// We have to set some values like Emscripten
|
|
||||||
memory.putInt(1024, stackTop)
|
memory.putInt(1024, stackTop)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun abort() { TODO() }
|
internal fun readCStringBytes(ptr: Int) = ByteArrayOutputStream().let { bos ->
|
||||||
|
var ptr = ptr
|
||||||
@WasmName("__syscall6")
|
while (true) {
|
||||||
fun close(arg0: Int, arg1: Int): Int { TODO() }
|
val byte = memory.get(ptr++)
|
||||||
|
if (byte == 0.toByte()) break
|
||||||
@WasmName("__syscall54")
|
bos.write(byte.toInt())
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
bos.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
@WasmName("__syscall140")
|
internal fun readCString(ptr: Int) = readCStringBytes(ptr).toString(Charsets.ISO_8859_1)
|
||||||
fun llseek(arg0: Int, arg1: Int): Int { TODO() }
|
|
||||||
|
fun abort() { TODO() }
|
||||||
|
|
||||||
@WasmName("__lock")
|
@WasmName("__lock")
|
||||||
fun lock(arg: Int) { TODO() }
|
fun lock(arg: Int) { TODO() }
|
||||||
@ -67,45 +48,20 @@ class Env(
|
|||||||
@WasmName("__unlock")
|
@WasmName("__unlock")
|
||||||
fun unlock(arg: Int) { TODO() }
|
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 {
|
companion object {
|
||||||
const val TOTAL_STACK = 5242880
|
const val TOTAL_STACK = 5242880
|
||||||
const val TOTAL_MEMORY = 16777216
|
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
|
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) {
|
fun warningInsteadOfErrReason(t: Throwable) = when (name) {
|
||||||
// NaN bit patterns can be off
|
// NaN bit patterns can be off
|
||||||
"float_literals", "float_exprs" ->
|
"float_literals", "float_exprs" ->
|
||||||
|
@ -3,6 +3,7 @@ package asmble.run.jvm
|
|||||||
import asmble.SpecTestUnit
|
import asmble.SpecTestUnit
|
||||||
import asmble.io.AstToSExpr
|
import asmble.io.AstToSExpr
|
||||||
import asmble.io.SExprToStr
|
import asmble.io.SExprToStr
|
||||||
|
import asmble.run.jvm.emscripten.Env
|
||||||
import asmble.util.Logger
|
import asmble.util.Logger
|
||||||
import org.junit.Assume
|
import org.junit.Assume
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -40,8 +41,12 @@ class RunTest(val unit: SpecTestUnit) : Logger by Logger.Print(Logger.Level.INFO
|
|||||||
logger = this,
|
logger = this,
|
||||||
adjustContext = { it.copy(eagerFailLargeMemOffset = false) },
|
adjustContext = { it.copy(eagerFailLargeMemOffset = false) },
|
||||||
defaultMaxMemPages = unit.defaultMaxMemPages
|
defaultMaxMemPages = unit.defaultMaxMemPages
|
||||||
).withHarnessRegistered(PrintWriter(OutputStreamWriter(out, Charsets.UTF_8), true)).
|
).withHarnessRegistered(PrintWriter(OutputStreamWriter(out, Charsets.UTF_8), true))
|
||||||
withEmscriptenEnvRegistered(out)
|
|
||||||
|
// 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
|
// This will fail assertions as necessary
|
||||||
scriptContext = unit.script.commands.fold(scriptContext) { scriptContext, cmd ->
|
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