Initial work to support emscripten runtime for issue #7

This commit is contained in:
Chad Retz
2017-04-26 15:35:34 -05:00
parent d94b5ce898
commit b4140c8189
31 changed files with 523 additions and 369 deletions

2
.gitignore vendored
View File

@ -5,3 +5,5 @@
/build /build
/gradle /gradle
/compiler/build /compiler/build
/emscripten-runtime/build
/annotations/build

View File

@ -0,0 +1,10 @@
package asmble.annotation;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface WasmName {
String value();
}

View File

@ -21,6 +21,12 @@ allprojects {
} }
} }
project(':emscripten-runtime') {
dependencies {
compile project(':annotations')
}
}
project(':compiler') { project(':compiler') {
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'application' apply plugin: 'application'
@ -32,6 +38,8 @@ project(':compiler') {
distZip.archiveName = 'asmble.zip' distZip.archiveName = 'asmble.zip'
dependencies { dependencies {
compile project(':emscripten-runtime')
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
compile "org.ow2.asm:asm-tree:$asm_version" compile "org.ow2.asm:asm-tree:$asm_version"

View File

@ -0,0 +1,21 @@
package asmble.io
open class Emscripten {
fun metadataFromWast(wast: String) =
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()?.let { staticBump ->
Metadata(staticBump)
}
}
}
}
data class Metadata(val staticBump: Int)
companion object : Emscripten()
}

View File

@ -1,9 +1,9 @@
package asmble.run.jvm package asmble.run.jvm
import asmble.annotation.WasmName
import asmble.ast.Node import asmble.ast.Node
import asmble.compile.jvm.Mem import asmble.compile.jvm.Mem
import asmble.compile.jvm.ref import asmble.compile.jvm.ref
import asmble.run.jvm.annotation.WasmName
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

View File

@ -4,6 +4,7 @@ import asmble.ast.Node
import asmble.ast.Script import asmble.ast.Script
import asmble.compile.jvm.* import asmble.compile.jvm.*
import asmble.io.AstToSExpr import asmble.io.AstToSExpr
import asmble.io.Emscripten
import asmble.io.SExprToStr import asmble.io.SExprToStr
import asmble.util.Logger import asmble.util.Logger
import asmble.util.toRawIntBits import asmble.util.toRawIntBits
@ -11,6 +12,8 @@ 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 run.jvm.emscripten.Env
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
@ -32,6 +35,14 @@ data class ScriptContext(
fun withHarnessRegistered(out: PrintWriter = PrintWriter(System.out, true)) = fun withHarnessRegistered(out: PrintWriter = PrintWriter(System.out, true)) =
withModuleRegistered("spectest", Module.Native(TestHarness(out))) withModuleRegistered("spectest", Module.Native(TestHarness(out)))
fun withEmscriptenRegistered(metadata: Emscripten.Metadata, out: OutputStream) =
Env(metadata.staticBump, out).let { env ->
val mods = Env.subModules.fold(listOf(Module.Native(env))) { mods, subMod ->
mods + Module.Native(subMod.apply(env))
}
withModuleRegistered("env", Module.Composite(mods))
}
fun withModuleRegistered(name: String, mod: Module) = copy(registrations = registrations + (name to mod)) fun withModuleRegistered(name: String, mod: Module) = copy(registrations = registrations + (name to mod))
fun runCommand(cmd: Script.Cmd) = when (cmd) { fun runCommand(cmd: Script.Cmd) = when (cmd) {

View File

@ -1,7 +1,7 @@
package asmble.run.jvm package asmble.run.jvm
import asmble.annotation.WasmName
import asmble.compile.jvm.Mem import asmble.compile.jvm.Mem
import asmble.run.jvm.annotation.WasmName
import java.io.PrintWriter import java.io.PrintWriter
import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandle
import java.nio.ByteBuffer import java.nio.ByteBuffer

View File

@ -1,4 +0,0 @@
package asmble.run.jvm.annotation
@Target(AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.FUNCTION)
annotation class WasmName(val value: String)

View File

@ -1,67 +0,0 @@
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 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)
init {
// Emscripten sets where "stack top" can start in mem at position 1024.
// See https://github.com/WebAssembly/binaryen/issues/979
val stackBase = alignTo16(staticBump + 1024 + 16)
val stackTop = stackBase + TOTAL_STACK
memory.putInt(1024, stackTop)
}
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()
}
internal fun readCString(ptr: Int) = readCStringBytes(ptr).toString(Charsets.ISO_8859_1)
fun abort() { TODO() }
@WasmName("__lock")
fun lock(arg: Int) { TODO() }
fun sbrk(increment: Int): Int { TODO() }
@WasmName("__unlock")
fun unlock(arg: Int) { TODO() }
companion object {
const val TOTAL_STACK = 5242880
const val TOTAL_MEMORY = 16777216
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

@ -1,3 +0,0 @@
package asmble.run.jvm.emscripten
open class Err(message: String, cause: Throwable? = null) : Exception(message, cause)

View File

@ -1,28 +0,0 @@
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

@ -1,15 +0,0 @@
package asmble.run.jvm.emscripten
sealed class Stream {
open val tty: Tty? = null
abstract fun write(bytes: ByteArray)
class OutputStream(val os: java.io.OutputStream) : Stream() {
override val tty by lazy { Tty.OutputStream(os) }
override fun write(bytes: ByteArray) {
os.write(bytes)
}
}
}

View File

@ -1,73 +0,0 @@
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

@ -1,7 +0,0 @@
package asmble.run.jvm.emscripten
sealed class Tty {
class OutputStream(val os: java.io.OutputStream) : Tty() {
}
}

View File

@ -0,0 +1,28 @@
package asmble
import asmble.ast.SExpr
import asmble.ast.Script
import asmble.io.Emscripten
import asmble.io.SExprToAst
import asmble.io.StrToSExpr
open class BaseTestUnit(val name: String, val wast: String, val expectedOutput: String?) {
override fun toString() = "Test unit: $name"
open val packageName = "asmble.temp." + name.replace('/', '.')
open val shouldFail get() = false
open val skipRunReason: String? get() = null
open val defaultMaxMemPages get() = 1
open val emscriptenMetadata by lazy { Emscripten.metadataFromWast(wast) }
open val parseResult: StrToSExpr.ParseResult.Success by lazy {
StrToSExpr.parse(wast).let {
when (it) {
is StrToSExpr.ParseResult.Error -> throw Exception("$name[${it.pos}] Parse fail: ${it.msg}")
is StrToSExpr.ParseResult.Success -> it
}
}
}
open val ast: List<SExpr> get() = parseResult.vals
open val script: Script by lazy { SExprToAst.toScript(SExpr.Multi(ast)) }
open fun warningInsteadOfErrReason(t: Throwable): String? = null
}

View File

@ -1,45 +1,25 @@
package asmble package asmble
import asmble.ast.Node import asmble.ast.Node
import asmble.ast.SExpr
import asmble.ast.Script import asmble.ast.Script
import asmble.io.SExprToAst
import asmble.io.StrToSExpr
import asmble.run.jvm.ScriptAssertionError import asmble.run.jvm.ScriptAssertionError
import java.nio.file.FileSystems import java.nio.file.FileSystems
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Paths import java.nio.file.Paths
import java.util.stream.Collectors import java.util.stream.Collectors
class SpecTestUnit(val name: String, val wast: String, val expectedOutput: String?) { class SpecTestUnit(name: String, wast: String, expectedOutput: String?) : BaseTestUnit(name, wast, expectedOutput) {
override fun toString() = "Spec unit: $name" override val shouldFail get() = name.endsWith(".fail")
val shouldFail get() = name.endsWith(".fail") override val defaultMaxMemPages get() = when (name) {
val skipRunReason: String? get() = null
val defaultMaxMemPages get() = when (name) {
"nop"-> 20 "nop"-> 20
"resizing" -> 830 "resizing" -> 830
"imports" -> 5 "imports" -> 5
else -> 1 else -> 1
} }
val emscriptenStaticBump by lazy { override fun warningInsteadOfErrReason(t: Throwable) = when (name) {
// 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 // NaN bit patterns can be off
"float_literals", "float_exprs" -> "float_literals", "float_exprs" ->
if (isNanMismatch(t)) "NaN JVM bit patterns can be off" else null if (isNanMismatch(t)) "NaN JVM bit patterns can be off" else null
@ -74,19 +54,6 @@ class SpecTestUnit(val name: String, val wast: String, val expectedOutput: Strin
else -> false else -> false
} }
val parseResult: StrToSExpr.ParseResult.Success by lazy {
StrToSExpr.parse(wast).let {
when (it) {
is StrToSExpr.ParseResult.Error -> throw Exception("$name[${it.pos}] Parse fail: ${it.msg}")
is StrToSExpr.ParseResult.Success -> it
}
}
}
val ast: List<SExpr> get() = parseResult.vals
val script: Script by lazy { SExprToAst.toScript(SExpr.Multi(ast)) }
companion object { companion object {
val unitsPath = "/spec/test/core" val unitsPath = "/spec/test/core"

View File

@ -0,0 +1,9 @@
package asmble
import asmble.util.Logger
abstract class TestBase : Logger by TestBase.logger {
companion object {
val logger = Logger.Print(Logger.Level.INFO)
}
}

View File

@ -1,76 +1,11 @@
package asmble.run.jvm package asmble.run.jvm
import asmble.SpecTestUnit 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
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
import java.io.ByteArrayOutputStream
import java.io.OutputStreamWriter
import java.io.PrintWriter
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@RunWith(Parameterized::class) @RunWith(Parameterized::class)
class RunTest(val unit: SpecTestUnit) : Logger by Logger.Print(Logger.Level.INFO) { class RunTest(unit: SpecTestUnit) : TestRunner<SpecTestUnit>(unit) {
@Test
fun testRun() {
unit.skipRunReason?.let { Assume.assumeTrue("Skipping ${unit.name}, reason: $it", false) }
val ex = try { run(); null } catch (e: Throwable) { e }
if (unit.shouldFail) {
assertNotNull(ex, "Expected failure, but succeeded")
debug { "Got expected failure: $ex" }
} else if (ex != null) throw ex
}
private fun run() {
debug { "AST SExpr: " + unit.ast }
debug { "AST Str: " + SExprToStr.fromSExpr(*unit.ast.toTypedArray()) }
debug { "AST: " + unit.script }
debug { "AST Str: " + SExprToStr.fromSExpr(*AstToSExpr.fromScript(unit.script).toTypedArray()) }
val out = ByteArrayOutputStream()
var scriptContext = ScriptContext(
packageName = "asmble.temp.${unit.name}",
logger = this,
adjustContext = { it.copy(eagerFailLargeMemOffset = false) },
defaultMaxMemPages = unit.defaultMaxMemPages
).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 ->
try {
scriptContext.runCommand(cmd)
} catch (t: Throwable) {
val warningReason = unit.warningInsteadOfErrReason(t) ?: throw t
warn { "Unexpected error on ${unit.name}, but is a warning. Reason: $warningReason. Orig err: $t" }
scriptContext
}
}
// If there is a main, we run it w/ no args because emscripten doesn't set it as the start func
scriptContext.modules.lastOrNull()?.also { lastMod ->
lastMod.cls.methods.find {
it.name == "main" &&
it.returnType == Int::class.java &&
it.parameterTypes.asList() == listOf(Int::class.java, Int::class.java)
}?.invoke(lastMod.instance(scriptContext), 0, 0)
}
unit.expectedOutput?.let { assertEquals(it, out.toByteArray().toString(Charsets.UTF_8)) }
}
companion object { companion object {
@JvmStatic @Parameterized.Parameters(name = "{0}") @JvmStatic @Parameterized.Parameters(name = "{0}")
fun data() = SpecTestUnit.allUnits fun data() = SpecTestUnit.allUnits

View File

@ -0,0 +1,72 @@
package asmble.run.jvm
import asmble.BaseTestUnit
import asmble.TestBase
import asmble.io.AstToSExpr
import asmble.io.SExprToStr
import org.junit.Assume
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.OutputStreamWriter
import java.io.PrintWriter
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
abstract class TestRunner<out T : BaseTestUnit>(val unit: T) : TestBase() {
@Test
fun test() {
unit.skipRunReason?.let { Assume.assumeTrue("Skipping ${unit.name}, reason: $it", false) }
val ex = try { run(); null } catch (e: Throwable) { e }
if (unit.shouldFail) {
assertNotNull(ex, "Expected failure, but succeeded")
debug { "Got expected failure: $ex" }
} else if (ex != null) throw ex
}
private fun run() {
debug { "AST SExpr: " + unit.ast }
debug { "AST Str: " + SExprToStr.fromSExpr(*unit.ast.toTypedArray()) }
debug { "AST: " + unit.script }
debug { "AST Str: " + SExprToStr.fromSExpr(*AstToSExpr.fromScript(unit.script).toTypedArray()) }
val out = ByteArrayOutputStream()
var scriptContext = ScriptContext(
packageName = unit.packageName,
logger = this,
adjustContext = { it.copy(eagerFailLargeMemOffset = false) },
defaultMaxMemPages = unit.defaultMaxMemPages
).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.emscriptenMetadata?.also { scriptContext = scriptContext.withEmscriptenRegistered(it, out) }
// This will fail assertions as necessary
scriptContext = unit.script.commands.fold(scriptContext) { scriptContext, cmd ->
try {
scriptContext.runCommand(cmd)
} catch (t: Throwable) {
val warningReason = unit.warningInsteadOfErrReason(t) ?: throw t
warn { "Unexpected error on ${unit.name}, but is a warning. Reason: $warningReason. Orig err: $t" }
scriptContext
}
}
// If there is a main, we run it w/ no args because emscripten doesn't set it as the start func
scriptContext.modules.lastOrNull()?.also { lastMod ->
lastMod.cls.methods.find { it.name == "main" && it.returnType == Int::class.java }?.let { mainMethod ->
if (mainMethod.parameterTypes.isEmpty())
mainMethod.invoke(lastMod.instance(scriptContext))
else if (mainMethod.parameterTypes.asList() == listOf(Int::class.java, Int::class.java))
mainMethod.invoke(lastMod.instance(scriptContext), 0, 0)
else
error("Unrecognized main method params for $mainMethod")
}
}
unit.expectedOutput?.let {
// Sadly, sometimes the expected output is trimmed in Emscripten tests
assertEquals(it.trimEnd(), out.toByteArray().toString(Charsets.UTF_8).trimEnd())
}
}
}

View File

@ -0,0 +1,20 @@
package asmble.run.jvm.emscripten
import asmble.run.jvm.TestRunner
import org.junit.BeforeClass
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
class EmscriptenTest(unit: EmscriptenTestUnit) : TestRunner<EmscriptenTestUnit>(unit) {
companion object {
var failureReason: Throwable? = null
@JvmStatic @BeforeClass
fun setup() { failureReason?.also { throw it } }
@JvmStatic @Parameterized.Parameters(name = "{0}")
fun data() = try { EmscriptenTestUnit.allUnits } catch (t: Throwable) { failureReason = t; listOf(null) }
}
}

View File

@ -0,0 +1,71 @@
package asmble.run.jvm.emscripten
import asmble.BaseTestUnit
import asmble.TestBase
import org.junit.Assert
import org.junit.AssumptionViolatedException
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.TimeUnit
import java.util.stream.Collectors
class EmscriptenTestUnit(
name: String,
wast: String,
expectedOutput: String
) : BaseTestUnit(name, wast, expectedOutput) {
companion object {
val knownGoodNames = listOf(
"core/test_addr_of_stacked"
)
val allUnits by lazy { loadUnits() }
fun loadUnits(): List<EmscriptenTestUnit> {
val wasmInstall = Paths.get(
System.getenv("WASM_INSTALL") ?: throw AssumptionViolatedException("WASM_INSTALL not set")
)
val testBase = wasmInstall.resolve("emscripten/tests")
fun Path.unitNameFromCFile() =
testBase.relativize(this).toString().substringBeforeLast(".c").replace('\\', '/')
// Obtain C files we know work
val goodCFiles = Files.walk(testBase).
filter { knownGoodNames.contains(it.unitNameFromCFile()) }.
collect(Collectors.toList())
// Go over each one and create a test unit
val tempDir = createTempDir("emscriptenout")
val isWindows = System.getProperty("os.name").contains("windows", true)
val emccCommand =
if (isWindows) arrayOf("cmd", "/c", wasmInstall.resolve("emscripten/emcc.bat").toString())
else arrayOf(wasmInstall.resolve("emscripten/emcc").toString())
try {
return goodCFiles.map { cFile ->
try {
// Run emcc on the cFile
val nameSansExt = cFile.fileName.toString().substringBeforeLast(".c")
val cmdArgs = emccCommand + cFile.toString() + "-s" + "WASM=1" + "-o" + "$nameSansExt.html"
TestBase.logger.debug { "Running ${cmdArgs.joinToString(" ")}" }
val proc = ProcessBuilder(*cmdArgs).
directory(tempDir).
redirectErrorStream(true).
also { it.environment() += "BINARYEN" to wasmInstall.toString() }.
start()
proc.inputStream.bufferedReader().forEachLine { TestBase.logger.debug { "[OUT] $it" } }
Assert.assertTrue("Timeout", proc.waitFor(10, TimeUnit.SECONDS))
Assert.assertEquals(0, proc.exitValue())
EmscriptenTestUnit(
name = cFile.unitNameFromCFile(),
wast = File(tempDir, "$nameSansExt.wast").readText(),
expectedOutput = cFile.resolveSibling("$nameSansExt.out").toFile().readText()
)
} catch (e: Exception) { throw Exception("Unable to compile $cFile", e) }
}
} finally {
try { tempDir.deleteRecursively() }
catch (e: Exception) { TestBase.logger.warn { "Unable to delete temp dir: $e" } }
}
}
}
}

View File

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

View File

@ -1,56 +0,0 @@
(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": [] }

View File

@ -0,0 +1,19 @@
package run.jvm.emscripten;
public class EmscriptenException extends RuntimeException {
public EmscriptenException() {
}
public EmscriptenException(String message) {
super(message);
}
public EmscriptenException(String message, Throwable cause) {
super(message, cause);
}
public EmscriptenException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,72 @@
package run.jvm.emscripten;
import asmble.annotation.WasmName;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
public class Env {
public static final int TOTAL_STACK = 5242880;
public static final int TOTAL_MEMORY = 16777216;
public static final List<Function<Env, Object>> subModules = Arrays.asList(
Syscall::new
);
private static int alignTo16(int num) {
return ((int) Math.ceil(num / 16.0)) * 16;
}
private final ByteBuffer memory;
private final int staticBump;
final OutputStream out;
public Env(int staticBump, OutputStream out) {
this(ByteBuffer.allocateDirect(TOTAL_MEMORY), staticBump, out);
}
public Env(ByteBuffer memory, int staticBump, OutputStream out) {
this.memory = memory.order(ByteOrder.LITTLE_ENDIAN);
this.staticBump = staticBump;
this.out = out;
// Emscripten sets where "stack top" can start in mem at position 1024.
// See https://github.com/WebAssembly/binaryen/issues/979
int stackBase = alignTo16(staticBump + 1024 + 16);
int stackTop = stackBase + TOTAL_STACK;
memory.putInt(1024, stackTop);
}
public ByteBuffer getMemory() {
return memory;
}
public byte[] getMemoryBulk(int index, int len) {
byte[] ret = new byte[len];
ByteBuffer dup = memory.duplicate();
dup.position(index);
dup.get(ret);
return ret;
}
public void abort() {
throw new UnsupportedOperationException();
}
@WasmName("__lock")
public void lock(int arg) {
throw new UnsupportedOperationException();
}
public int sbrk(int increment) {
throw new UnsupportedOperationException();
}
@WasmName("__unlock")
public void unlock(int arg) {
throw new UnsupportedOperationException();
}
}

View File

@ -1,6 +1,6 @@
package asmble.run.jvm.emscripten package run.jvm.emscripten;
enum class Errno(val number: Int) { public enum Errno {
EPERM(1), EPERM(1),
ENOENT(2), ENOENT(2),
ESRCH(3), ESRCH(3),
@ -136,7 +136,30 @@ enum class Errno(val number: Int) {
ERFKILL(132), ERFKILL(132),
EHWPOISON(133); EHWPOISON(133);
fun raise(cause: Throwable? = null): Nothing = throw Err(this, cause) final int number;
class Err(errno: Errno, cause: Throwable? = null) : asmble.run.jvm.emscripten.Err("Errno: $errno", cause) Errno(int number) {
this.number = number;
}
public void raise() {
raise(null);
}
public void raise(Throwable cause) {
throw new ErrnoException(this, cause);
}
public static class ErrnoException extends EmscriptenException {
public final Errno errno;
public ErrnoException(Errno errno) {
this(errno, null);
}
public ErrnoException(Errno errno, Throwable cause) {
super("Errno: " + errno, cause);
this.errno = errno;
}
}
} }

View File

@ -0,0 +1,31 @@
package run.jvm.emscripten;
import java.io.IOException;
public abstract class FStream {
public abstract Tty getTty();
public abstract void write(byte[] bytes);
public static class OutputStream extends FStream {
private final Tty.OutputStream tty;
public OutputStream(java.io.OutputStream out) {
this.tty = new Tty.OutputStream(out);
}
@Override
public Tty.OutputStream getTty() {
return tty;
}
@Override
public void write(byte[] bytes) {
try {
tty.out.write(bytes);
} catch (IOException e) {
throw new EmscriptenException(e);
}
}
}
}

View File

@ -0,0 +1,101 @@
package run.jvm.emscripten;
import asmble.annotation.WasmName;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class Syscall {
private final Map<Integer, FStream> fds;
private final Env env;
public Syscall(Env env) {
this.fds = new HashMap<>();
fds.put(1, new FStream.OutputStream(env.out));
this.env = env;
}
@WasmName("__syscall6")
public int close(int arg0, int arg1) {
throw new UnsupportedOperationException();
}
@WasmName("__syscall54")
public int ioctl(int which, int varargs) {
FStream fd = fd(env.getMemory().getInt(varargs));
IoctlOp op = IoctlOp.byNumber.get(env.getMemory().getInt(varargs + 4));
Objects.requireNonNull(op);
switch (op) {
case TCGETS:
case TCSETS:
case TIOCGWINSZ:
return fd.getTty() == null ? -Errno.ENOTTY.number : 0;
case TIOCGPGRP:
if (fd.getTty() == null) return -Errno.ENOTTY.number;
env.getMemory().putInt(env.getMemory().getInt(varargs + 8), 0);
return 0;
case TIOCSPGRP:
return fd.getTty() == null ? -Errno.ENOTTY.number : -Errno.EINVAL.number;
case FIONREAD:
if (fd.getTty() == null) return -Errno.ENOTTY.number;
throw new UnsupportedOperationException("TODO");
default:
throw new EmscriptenException("Unrecognized op: " + op);
}
}
@WasmName("__syscall140")
public int llseek(int arg0, int arg1) {
throw new UnsupportedOperationException();
}
//
@WasmName("__syscall146")
public int writev(int which, int varargs) {
FStream fd = fd(env.getMemory().getInt(varargs));
int iov = env.getMemory().getInt(varargs + 4);
int iovcnt = env.getMemory().getInt(varargs + 8);
return IntStream.range(0, iovcnt).reduce(0, (total, i) -> {
int ptr = env.getMemory().getInt(iov + (i * 8));
int len = env.getMemory().getInt(iov + (i * 8) + 4);
if (len > 0) fd.write(env.getMemoryBulk(ptr, len));
return total + len;
});
}
private FStream fd(int v) {
FStream ret = fds.get(v);
if (ret == null) Errno.EBADF.raise();
return ret;
}
public static enum IoctlOp {
TCGETS(0x5401),
TCSETS(0x5402),
TIOCGPGRP(0x540F),
TIOCSPGRP(0x5410),
FIONREAD(0x541B),
TIOCGWINSZ(0x5413);
static final Map<Integer, IoctlOp> byNumber;
static {
byNumber = Stream.of(values()).collect(Collectors.toMap(IoctlOp::getNumber, Function.identity()));
}
final int number;
IoctlOp(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
}
}

View File

@ -0,0 +1,12 @@
package run.jvm.emscripten;
public abstract class Tty {
public static class OutputStream extends Tty {
final java.io.OutputStream out;
public OutputStream(java.io.OutputStream out) {
this.out = out;
}
}
}

View File

@ -1,2 +1,2 @@
rootProject.name = 'asmble' rootProject.name = 'asmble'
include 'compiler' include 'annotations', 'compiler', 'emscripten-runtime'