Beginning of emscripten emulation

This commit is contained in:
Chad Retz
2017-04-20 23:35:02 -05:00
parent da1d94dc9e
commit 5890a1cd7c
12 changed files with 21061 additions and 14 deletions

View File

@ -6,12 +6,14 @@ 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
@ -36,6 +38,9 @@ data class ScriptContext(
"spectest" to NativeModule(TestHarness::class.java, TestHarness(out))
))
fun withEmscriptenEnvRegistered(out: OutputStream = System.out) =
copy(registrations = registrations + ("env" to NativeModule(Env::class.java, Env(logger, out))))
fun runCommand(cmd: Script.Cmd) = when (cmd) {
is Script.Cmd.Module ->
// We ask for the module instance because some things are built on <init> expectation

View File

@ -0,0 +1,111 @@
package asmble.run.jvm.emscripten
import asmble.compile.jvm.Mem
import asmble.run.jvm.annotation.WasmName
import asmble.util.Logger
import asmble.util.get
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
class Env(
val logger: Logger,
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
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")
}
}
@WasmName("__syscall140")
fun llseek(arg0: Int, arg1: Int): Int { TODO() }
@WasmName("__lock")
fun lock(arg: Int) { TODO() }
fun sbrk(increment: Int): Int { TODO() }
@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
}
}

View File

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

View File

@ -0,0 +1,142 @@
package asmble.run.jvm.emscripten
enum class Errno(val number: Int) {
EPERM(1),
ENOENT(2),
ESRCH(3),
EINTR(4),
EIO(5),
ENXIO(6),
E2BIG(7),
ENOEXEC(8),
EBADF(9),
ECHILD(10),
EAGAIN(11),
ENOMEM(12),
EACCES(13),
EFAULT(14),
ENOTBLK(15),
EBUSY(16),
EEXIST(17),
EXDEV(18),
ENODEV(19),
ENOTDIR(20),
EISDIR(21),
EINVAL(22),
ENFILE(23),
EMFILE(24),
ENOTTY(25),
ETXTBSY(26),
EFBIG(27),
ENOSPC(28),
ESPIPE(29),
EROFS(30),
EMLINK(31),
EPIPE(32),
EDOM(33),
ERANGE(34),
EDEADLK(35),
ENAMETOOLONG(36),
ENOLCK(37),
ENOSYS(38),
ENOTEMPTY(39),
ELOOP(40),
EWOULDBLOCK(EAGAIN.number),
ENOMSG(42),
EIDRM(43),
ECHRNG(44),
EL2NSYNC(45),
EL3HLT(46),
EL3RST(47),
ELNRNG(48),
EUNATCH(49),
ENOCSI(50),
EL2HLT(51),
EBADE(52),
EBADR(53),
EXFULL(54),
ENOANO(55),
EBADRQC(56),
EBADSLT(57),
EDEADLOCK(EDEADLK.number),
EBFONT(59),
ENOSTR(60),
ENODATA(61),
ETIME(62),
ENOSR(63),
ENONET(64),
ENOPKG(65),
EREMOTE(66),
ENOLINK(67),
EADV(68),
ESRMNT(69),
ECOMM(70),
EPROTO(71),
EMULTIHOP(72),
EDOTDOT(73),
EBADMSG(74),
EOVERFLOW(75),
ENOTUNIQ(76),
EBADFD(77),
EREMCHG(78),
ELIBACC(79),
ELIBBAD(80),
ELIBSCN(81),
ELIBMAX(82),
ELIBEXEC(83),
EILSEQ(84),
ERESTART(85),
ESTRPIPE(86),
EUSERS(87),
ENOTSOCK(88),
EDESTADDRREQ(89),
EMSGSIZE(90),
EPROTOTYPE(91),
ENOPROTOOPT(92),
EPROTONOSUPPORT(93),
ESOCKTNOSUPPORT(94),
EOPNOTSUPP(95),
ENOTSUP(EOPNOTSUPP.number),
EPFNOSUPPORT(96),
EAFNOSUPPORT(97),
EADDRINUSE(98),
EADDRNOTAVAIL(99),
ENETDOWN(100),
ENETUNREACH(101),
ENETRESET(102),
ECONNABORTED(103),
ECONNRESET(104),
ENOBUFS(105),
EISCONN(106),
ENOTCONN(107),
ESHUTDOWN(108),
ETOOMANYREFS(109),
ETIMEDOUT(110),
ECONNREFUSED(111),
EHOSTDOWN(112),
EHOSTUNREACH(113),
EALREADY(114),
EINPROGRESS(115),
ESTALE(116),
EUCLEAN(117),
ENOTNAM(118),
ENAVAIL(119),
EISNAM(120),
EREMOTEIO(121),
EDQUOT(122),
ENOMEDIUM(123),
EMEDIUMTYPE(124),
ECANCELED(125),
ENOKEY(126),
EKEYEXPIRED(127),
EKEYREVOKED(128),
EKEYREJECTED(129),
EOWNERDEAD(130),
ENOTRECOVERABLE(131),
ERFKILL(132),
EHWPOISON(133);
fun raise(cause: Throwable? = null): Nothing = throw Err(this, cause)
class Err(errno: Errno, cause: Throwable? = null) : asmble.run.jvm.emscripten.Err("Errno: $errno", cause)
}

View File

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

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

View File

@ -0,0 +1,5 @@
package asmble.util
import java.nio.ByteBuffer
fun ByteBuffer.get(index: Int, bytes: ByteArray) = this.duplicate().also { it.position(index) }.get(bytes)

View File

@ -8,8 +8,9 @@ import org.junit.Assume
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.ByteArrayOutputStream
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.io.StringWriter
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@ -33,16 +34,17 @@ class RunTest(val unit: SpecTestUnit) : Logger by Logger.Print(Logger.Level.INFO
debug { "AST: " + unit.script }
debug { "AST Str: " + SExprToStr.fromSExpr(*AstToSExpr.fromScript(unit.script).toTypedArray()) }
val out = StringWriter()
val scriptContext = ScriptContext(
val out = ByteArrayOutputStream()
var scriptContext = ScriptContext(
packageName = "asmble.temp.${unit.name}",
logger = this,
adjustContext = { it.copy(eagerFailLargeMemOffset = false) },
defaultMaxMemPages = unit.defaultMaxMemPages
).withHarnessRegistered(PrintWriter(out))
).withHarnessRegistered(PrintWriter(OutputStreamWriter(out, Charsets.UTF_8), true)).
withEmscriptenEnvRegistered(out)
// This will fail assertions as necessary
unit.script.commands.fold(scriptContext) { scriptContext, cmd ->
scriptContext = unit.script.commands.fold(scriptContext) { scriptContext, cmd ->
try {
scriptContext.runCommand(cmd)
} catch (t: Throwable) {
@ -52,7 +54,16 @@ class RunTest(val unit: SpecTestUnit) : Logger by Logger.Print(Logger.Level.INFO
}
}
unit.expectedOutput?.let { assertEquals(it, out.toString()) }
// 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 {

View File

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

View File

@ -1,8 +0,0 @@
;; TODO: test w/ emscripten compiled code
(module
(import "spectest" "print" (func $print (param i32)))
(func (export "printNum") (param $i i32)
(call $print (get_local $i))
)
)

File diff suppressed because it is too large Load Diff