about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2023-08-29 22:53:25 +0200
committerbptato <nincsnevem662@gmail.com>2023-08-29 22:53:25 +0200
commite44e5c93f698f5ddf3168cfcc87a4494ad91641d (patch)
treee00f31b3808733fbb3e2c016c56c1ce412db58f2
parenta0af8402e3cceea30ec571660faa7fb1032e7527 (diff)
downloadchawan-e44e5c93f698f5ddf3168cfcc87a4494ad91641d.tar.gz
javascript: factor out fromJS
-rw-r--r--src/display/client.nim5
-rw-r--r--src/html/chadombuilder.nim1
-rw-r--r--src/html/dom.nim3
-rw-r--r--src/html/event.nim1
-rw-r--r--src/io/headers.nim1
-rw-r--r--src/io/request.nim1
-rw-r--r--src/js/fromjs.nim457
-rw-r--r--src/js/javascript.nim493
-rw-r--r--src/js/opaque.nim37
-rw-r--r--src/types/blob.nim1
-rw-r--r--src/types/color.nim1
11 files changed, 509 insertions, 492 deletions
diff --git a/src/display/client.nim b/src/display/client.nim
index c437b75b..fe973b9c 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -35,6 +35,7 @@ import ips/serialize
 import ips/serversocket
 import ips/socketstream
 import js/domexception
+import js/fromjs
 import js/intl
 import js/javascript
 import js/module
@@ -144,8 +145,8 @@ proc command0(client: Client, src: string, filename = "<command>",
     client.jsctx.writeException(client.console.err)
   else:
     if not silence:
-      let str = toString(client.jsctx, ret)
-      if str.issome:
+      let str = fromJS[string](client.jsctx, ret)
+      if str.isSome:
         client.console.err.write(str.get & '\n')
         client.console.err.flush()
   JS_FreeValue(client.jsctx, ret)
diff --git a/src/html/chadombuilder.nim b/src/html/chadombuilder.nim
index ad8f7f36..5ccc7ee2 100644
--- a/src/html/chadombuilder.nim
+++ b/src/html/chadombuilder.nim
@@ -4,6 +4,7 @@ import streams
 
 import html/dom
 import js/error
+import js/fromjs
 import js/javascript
 import types/url
 
diff --git a/src/html/dom.nim b/src/html/dom.nim
index c0b60728..617ec404 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -20,6 +20,7 @@ import io/request
 import io/window
 import js/domexception
 import js/error
+import js/fromjs
 import js/javascript
 import js/opaque
 import js/timeout
@@ -2943,7 +2944,7 @@ proc jsReflectSet(ctx: JSContext, this, val: JSValue, magic: cint): JSValue {.cd
     return JS_ThrowTypeError(ctx, "Invalid tag type %s", element.tagType)
   case entry.t
   of REFLECT_STR:
-    let x = toString(ctx, val)
+    let x = fromJS[string](ctx, val)
     if x.isSome:
       element.attr(entry.attrname, x.get)
   of REFLECT_BOOL:
diff --git a/src/html/event.nim b/src/html/event.nim
index 0584b230..5f841fd1 100644
--- a/src/html/event.nim
+++ b/src/html/event.nim
@@ -3,6 +3,7 @@ import times
 
 import bindings/quickjs
 import js/error
+import js/fromjs
 import js/javascript
 import js/tojs
 import utils/opt
diff --git a/src/io/headers.nim b/src/io/headers.nim
index 7db3d8c4..e0dacf33 100644
--- a/src/io/headers.nim
+++ b/src/io/headers.nim
@@ -1,5 +1,6 @@
 import tables
 
+import js/fromjs
 import js/javascript
 import utils/twtstr
 
diff --git a/src/io/request.nim b/src/io/request.nim
index 6b89eb6b..211246f1 100644
--- a/src/io/request.nim
+++ b/src/io/request.nim
@@ -6,6 +6,7 @@ import tables
 import bindings/quickjs
 import io/headers
 import js/error
+import js/fromjs
 import js/javascript
 import types/formdata
 import types/url
diff --git a/src/js/fromjs.nim b/src/js/fromjs.nim
new file mode 100644
index 00000000..9c356da0
--- /dev/null
+++ b/src/js/fromjs.nim
@@ -0,0 +1,457 @@
+import macros
+import options
+import strutils
+import tables
+import unicode
+
+import bindings/quickjs
+import js/error
+import js/opaque
+import js/tojs
+import utils/opt
+
+proc fromJS*[T](ctx: JSContext, val: JSValue): JSResult[T]
+
+func isInstanceOf*(ctx: JSContext, val: JSValue, class: string): bool =
+  let ctxOpaque = ctx.getOpaque()
+  var classid = JS_GetClassID(val)
+  let tclassid = ctxOpaque.creg[class]
+  var found = false
+  while true:
+    if classid == tclassid:
+      found = true
+      break
+    ctxOpaque.parents.withValue(classid, val):
+      classid = val[]
+    do:
+      classid = 0 # not defined by Chawan; assume parent is Object.
+    if classid == 0:
+      break
+  return found
+
+func toString(ctx: JSContext, val: JSValue): Opt[string] =
+  var plen: csize_t
+  let outp = JS_ToCStringLen(ctx, addr plen, val) # cstring
+  if outp != nil:
+    var ret = newString(plen)
+    if plen != 0:
+      prepareMutation(ret)
+      copyMem(addr ret[0], outp, plen)
+    result = ok(ret)
+    JS_FreeCString(ctx, outp)
+
+func fromJSString(ctx: JSContext, val: JSValue): JSResult[string] =
+  var plen: csize_t
+  let outp = JS_ToCStringLen(ctx, addr plen, val) # cstring
+  if outp == nil:
+    return err()
+  var ret = newString(plen)
+  if plen != 0:
+    prepareMutation(ret)
+    copyMem(addr ret[0], outp, plen)
+  JS_FreeCString(ctx, outp)
+  return ok(ret)
+
+func fromJSInt[T: SomeInteger](ctx: JSContext, val: JSValue):
+    JSResult[T] =
+  if not JS_IsNumber(val):
+    return err()
+  when T is int:
+    # Always int32, so we don't risk 32-bit only breakage.
+    # If int64 is needed, specify it explicitly.
+    var ret: int32
+    if JS_ToInt32(ctx, addr ret, val) < 0:
+      return err()
+    return ok(int(ret))
+  elif T is uint:
+    var ret: uint32
+    if JS_ToUint32(ctx, addr ret, val) < 0:
+      return err()
+    return ok(uint(ret))
+  elif T is int32:
+    var ret: int32
+    if JS_ToInt32(ctx, addr ret, val) < 0:
+      return err()
+    return ok(ret)
+  elif T is int64:
+    var ret: int64
+    if JS_ToInt64(ctx, addr ret, val) < 0:
+      return err()
+    return ok(ret)
+  elif T is uint32:
+    var ret: uint32
+    if JS_ToUint32(ctx, addr ret, val) < 0:
+      return err()
+    return ok(ret)
+  elif T is uint64:
+    var ret: uint32
+    if JS_ToUint32(ctx, addr ret, val) < 0:
+      return err()
+    return ok(cast[uint64](ret))
+
+proc fromJSFloat[T: SomeFloat](ctx: JSContext, val: JSValue):
+    JSResult[T] =
+  if not JS_IsNumber(val):
+    return err()
+  var f64: float64
+  if JS_ToFloat64(ctx, addr f64, val) < 0:
+    return err()
+  return ok(cast[T](f64))
+
+macro fromJSTupleBody(a: tuple) =
+  let len = a.getType().len - 1
+  let done = ident("done")
+  result = newStmtList(quote do:
+    var `done`: bool)
+  for i in 0..<len:
+    result.add(quote do:
+      let next = JS_Call(ctx, next_method, it, 0, nil)
+      if JS_IsException(next):
+        return err()
+      defer: JS_FreeValue(ctx, next)
+      let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE])
+      if JS_IsException(doneVal):
+        return err()
+      defer: JS_FreeValue(ctx, doneVal)
+      `done` = ?fromJS[bool](ctx, doneVal)
+      if `done`:
+        JS_ThrowTypeError(ctx, "Too few arguments in sequence (got %d, expected %d)", `i`, `len`)
+        return err()
+      let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[VALUE])
+      if JS_IsException(valueVal):
+        return err()
+      defer: JS_FreeValue(ctx, valueVal)
+      let genericRes = fromJS[typeof(`a`[`i`])](ctx, valueVal)
+      if genericRes.isErr: # exception
+        return err()
+      `a`[`i`] = genericRes.get
+    )
+    if i == len - 1:
+      result.add(quote do:
+        let next = JS_Call(ctx, next_method, it, 0, nil)
+        if JS_IsException(next):
+          return err()
+        defer: JS_FreeValue(ctx, next)
+        let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE])
+        `done` = ?fromJS[bool](ctx, doneVal)
+        var i = `i`
+        # we're emulating a sequence, so we must query all remaining parameters too:
+        while not `done`:
+          inc i
+          let next = JS_Call(ctx, next_method, it, 0, nil)
+          if JS_IsException(next):
+            return err()
+          defer: JS_FreeValue(ctx, next)
+          let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE])
+          if JS_IsException(doneVal):
+            return err()
+          defer: JS_FreeValue(ctx, doneVal)
+          `done` = ?fromJS[bool](ctx, doneVal)
+          if `done`:
+            let msg = "Too many arguments in sequence (got " & $i &
+              ", expected " & $`len` & ")"
+            return err(newTypeError(msg))
+          JS_FreeValue(ctx, JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[VALUE]))
+      )
+
+proc fromJSTuple[T: tuple](ctx: JSContext, val: JSValue): JSResult[T] =
+  let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_refs[ITERATOR])
+  if JS_IsException(itprop):
+    return err()
+  defer: JS_FreeValue(ctx, itprop)
+  let it = JS_Call(ctx, itprop, val, 0, nil)
+  if JS_IsException(it):
+    return err()
+  defer: JS_FreeValue(ctx, it)
+  let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().str_refs[NEXT])
+  if JS_IsException(next_method):
+    return err()
+  defer: JS_FreeValue(ctx, next_method)
+  var x: T
+  fromJSTupleBody(x)
+  return ok(x)
+
+proc fromJSSeq[T](ctx: JSContext, val: JSValue): JSResult[seq[T]] =
+  let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_refs[ITERATOR])
+  if JS_IsException(itprop):
+    return err()
+  defer: JS_FreeValue(ctx, itprop)
+  let it = JS_Call(ctx, itprop, val, 0, nil)
+  if JS_IsException(it):
+    return err()
+  defer: JS_FreeValue(ctx, it)
+  let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().str_refs[NEXT])
+  if JS_IsException(next_method):
+    return err()
+  defer: JS_FreeValue(ctx, next_method)
+  var s = newSeq[T]()
+  while true:
+    let next = JS_Call(ctx, next_method, it, 0, nil)
+    if JS_IsException(next):
+      return err()
+    defer: JS_FreeValue(ctx, next)
+    let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE])
+    if JS_IsException(doneVal):
+      return err()
+    defer: JS_FreeValue(ctx, doneVal)
+    let done = ?fromJS[bool](ctx, doneVal)
+    if done:
+      break
+    let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[VALUE])
+    if JS_IsException(valueVal):
+      return err()
+    defer: JS_FreeValue(ctx, valueVal)
+    let genericRes = fromJS[typeof(s[0])](ctx, valueVal)
+    if genericRes.isnone: # exception
+      return err()
+    s.add(genericRes.get)
+  return ok(s)
+
+proc fromJSSet[T](ctx: JSContext, val: JSValue): Opt[set[T]] =
+  let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_refs[ITERATOR])
+  if JS_IsException(itprop):
+    return err()
+  defer: JS_FreeValue(ctx, itprop)
+  let it = JS_Call(ctx, itprop, val, 0, nil)
+  if JS_IsException(it):
+    return err()
+  defer: JS_FreeValue(ctx, it)
+  let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().str_refs[NEXT])
+  if JS_IsException(next_method):
+    return err()
+  defer: JS_FreeValue(ctx, next_method)
+  var s: set[T]
+  while true:
+    let next = JS_Call(ctx, next_method, it, 0, nil)
+    if JS_IsException(next):
+      return err()
+    defer: JS_FreeValue(ctx, next)
+    let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().done)
+    if JS_IsException(doneVal):
+      return err()
+    defer: JS_FreeValue(ctx, doneVal)
+    let done = ?fromJS[bool](ctx, doneVal)
+    if done:
+      break
+    let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().value)
+    if JS_IsException(valueVal):
+      return err()
+    defer: JS_FreeValue(ctx, valueVal)
+    let genericRes = ?fromJS[typeof(s.items)](ctx, valueVal)
+    s.incl(genericRes)
+  return ok(s)
+
+proc fromJSTable[A, B](ctx: JSContext, val: JSValue): JSResult[Table[A, B]] =
+  var ptab: ptr UncheckedArray[JSPropertyEnum]
+  var plen: uint32
+  let flags = cint(JS_GPN_STRING_MASK)
+  if JS_GetOwnPropertyNames(ctx, addr ptab, addr plen, val, flags) == -1:
+    # exception
+    return err()
+  defer:
+    for i in 0 ..< plen:
+      JS_FreeAtom(ctx, ptab[i].atom)
+    js_free(ctx, ptab)
+  var res = Table[A, B]()
+  for i in 0 ..< plen:
+    let atom = ptab[i].atom
+    let k = JS_AtomToValue(ctx, atom)
+    defer: JS_FreeValue(ctx, k)
+    let kn = ?fromJS[A](ctx, k)
+    let v = JS_GetProperty(ctx, val, atom)
+    defer: JS_FreeValue(ctx, v)
+    let vn = ?fromJS[B](ctx, v)
+    res[kn] = vn
+  return ok(res)
+
+#TODO varargs
+proc fromJSFunction1*[T, U](ctx: JSContext, val: JSValue):
+    proc(x: U): JSResult[T] =
+  return proc(x: U): JSResult[T] =
+    var arg1 = toJS(ctx, x)
+    #TODO exceptions?
+    let ret = JS_Call(ctx, val, JS_UNDEFINED, 1, addr arg1)
+    when T isnot void:
+      result = fromJS[T](ctx, ret)
+    JS_FreeValue(ctx, ret)
+
+proc isErrType(rt: NimNode): bool =
+  let rtType = rt[0]
+  let errType = getTypeInst(Err)
+  return errType.sameType(rtType) and rtType.sameType(errType)
+
+# unpack brackets
+proc getRealTypeFun(x: NimNode): NimNode =
+  var x = x.getTypeImpl()
+  while true:
+    if x.kind == nnkBracketExpr and x.len == 2:
+      x = x[1].getTypeImpl()
+      continue
+    break
+  return x
+
+macro unpackReturnType(f: typed) =
+  var x = f.getRealTypeFun()
+  let params = x.findChild(it.kind == nnkFormalParams)
+  let rv = params[0]
+  if rv.isErrType():
+    return quote do: void
+  let rvv = rv[1]
+  return quote do: `rvv`
+
+macro unpackArg0(f: typed) =
+  var x = f.getRealTypeFun()
+  let params = x.findChild(it.kind == nnkFormalParams)
+  let rv = params[1]
+  doAssert rv.kind == nnkIdentDefs
+  let rvv = rv[1]
+  return quote do: `rvv`
+
+proc fromJSFunction[T](ctx: JSContext, val: JSValue):
+    JSResult[T] =
+  #TODO all args...
+  return ok(
+    fromJSFunction1[
+      typeof(unpackReturnType(T)),
+      typeof(unpackArg0(T))
+    ](ctx, val))
+
+proc fromJSChar(ctx: JSContext, val: JSValue): Opt[char] =
+  let s = ?toString(ctx, val)
+  if s.len > 1:
+    return err()
+  return ok(s[0])
+
+proc fromJSRune(ctx: JSContext, val: JSValue): Opt[Rune] =
+  let s = ?toString(ctx, val)
+  var i = 0
+  var r: Rune
+  fastRuneAt(s, i, r)
+  if i < s.len:
+    return err()
+  return ok(r)
+
+template optionType[T](o: type Option[T]): auto =
+  T
+
+# wrap
+proc fromJSOption[T](ctx: JSContext, val: JSValue): JSResult[Option[T]] =
+  if JS_IsUndefined(val):
+    #TODO what about null?
+    return err()
+  let res = ?fromJS[T](ctx, val)
+  return ok(some(res))
+
+# wrap
+proc fromJSOpt[T](ctx: JSContext, val: JSValue): JSResult[T] =
+  if JS_IsUndefined(val):
+    #TODO what about null?
+    return err()
+  let res = fromJS[T.valType](ctx, val)
+  if res.isErr:
+    return ok(opt(T.valType))
+  return ok(opt(res.get))
+
+proc fromJSBool(ctx: JSContext, val: JSValue): JSResult[bool] =
+  let ret = JS_ToBool(ctx, val)
+  if ret == -1: # exception
+    return err()
+  if ret == 0:
+    return ok(false)
+  return ok(true)
+
+proc fromJSEnum[T: enum](ctx: JSContext, val: JSValue): JSResult[T] =
+  if JS_IsException(val):
+    return err()
+  let s = ?toString(ctx, val)
+  try:
+    return ok(parseEnum[T](s))
+  except ValueError:
+    return err(newTypeError("`" & s &
+      "' is not a valid value for enumeration " & $T))
+
+proc fromJSPObj0(ctx: JSContext, val: JSValue, t: string):
+    JSResult[pointer] =
+  if JS_IsException(val):
+    return err(nil)
+  if JS_IsNull(val):
+    return ok(nil)
+  let ctxOpaque = ctx.getOpaque()
+  if ctxOpaque.gclaz == t:
+    return ok(?getGlobalOpaque0(ctx, val))
+  if not JS_IsObject(val):
+    return err(newTypeError("Value is not an object"))
+  if not isInstanceOf(ctx, val, t):
+    let errmsg = t & " expected"
+    JS_ThrowTypeError(ctx, cstring(errmsg))
+    return err(newTypeError(errmsg))
+  let classid = JS_GetClassID(val)
+  let op = JS_GetOpaque(val, classid)
+  return ok(op)
+
+proc fromJSObject[T: ref object](ctx: JSContext, val: JSValue): JSResult[T] =
+  return ok(cast[T](?fromJSPObj0(ctx, val, $T)))
+
+type FromJSAllowedT = (object and not (Result|Option|Table|JSValue))
+
+proc fromJS*[T](ctx: JSContext, val: JSValue): JSResult[T] =
+  when T is string:
+    return fromJSString(ctx, val)
+  elif T is char:
+    return fromJSChar(ctx, val)
+  elif T is Rune:
+    return fromJSRune(ctx, val)
+  elif T is (proc):
+    return fromJSFunction[T](ctx, val)
+  elif T is Option:
+    return fromJSOption[optionType(T)](ctx, val)
+  elif T is Opt: # unwrap
+    return fromJSOpt[T](ctx, val)
+  elif T is seq:
+    return fromJSSeq[typeof(result.get.items)](ctx, val)
+  elif T is set:
+    return fromJSSet[typeof(result.get.items)](ctx, val)
+  elif T is tuple:
+    return fromJSTuple[T](ctx, val)
+  elif T is bool:
+    return fromJSBool(ctx, val)
+  elif typeof(result).valType is Table:
+    return fromJSTable[typeof(result.get.keys),
+      typeof(result.get.values)](ctx, val)
+  elif T is SomeInteger:
+    return fromJSInt[T](ctx, val)
+  elif T is SomeFloat:
+    return fromJSFloat[T](ctx, val)
+  elif T is enum:
+    return fromJSEnum[T](ctx, val)
+  elif T is JSValue:
+    return ok(val)
+  elif T is ref object:
+    return fromJSObject[T](ctx, val)
+  elif compiles(fromJS2(ctx, val, result)):
+    fromJS2(ctx, val, result)
+  else:
+    static:
+      error("Unrecognized type " & $T)
+
+const JS_ATOM_TAG_INT = cuint(1u32 shl 31)
+
+func JS_IsNumber*(v: JSAtom): JS_BOOL =
+  return (cast[cuint](v) and JS_ATOM_TAG_INT) != 0
+
+func fromJS*[T: string|uint32](ctx: JSContext, atom: JSAtom): Opt[T] =
+  when T is SomeNumber:
+    if JS_IsNumber(atom):
+      return ok(T(cast[uint32](atom) and (not JS_ATOM_TAG_INT)))
+  else:
+    let val = JS_AtomToValue(ctx, atom)
+    return toString(ctx, val)
+
+proc fromJSPObj[T](ctx: JSContext, val: JSValue): JSResult[ptr T] =
+  return cast[JSResult[ptr T]](fromJSPObj0(ctx, val, $T))
+
+template fromJSP*[T](ctx: JSContext, val: JSValue): untyped =
+  when T is FromJSAllowedT:
+    fromJSPObj[T](ctx, val)
+  else:
+    fromJS[T](ctx, val)
diff --git a/src/js/javascript.nim b/src/js/javascript.nim
index e9c88292..f7b50cfb 100644
--- a/src/js/javascript.nim
+++ b/src/js/javascript.nim
@@ -43,6 +43,7 @@ import tables
 import unicode
 
 import js/error
+import js/fromjs
 import js/opaque
 import js/tojs
 import js/typeptr
@@ -163,65 +164,18 @@ proc setGlobal*[T](ctx: JSContext, global: JSValue, obj: T) =
   ctx.setOpaque(global, obj)
   GC_ref(obj)
 
-func isGlobal*(ctx: JSContext, class: string): bool =
-  assert class != ""
-  return ctx.getOpaque().gclaz == class
-
-# getOpaque, but doesn't work for global objects.
-func getOpaque0*(val: JSValue): pointer =
-  if JS_VALUE_GET_TAG(val) == JS_TAG_OBJECT:
-    return JS_GetOpaque(val, JS_GetClassID(val))
-
-func getGlobalOpaque0(ctx: JSContext, val: JSValue = JS_UNDEFINED):
-    Opt[pointer] =
-  let global = JS_GetGlobalObject(ctx)
-  if JS_IsUndefined(val) or val == global:
-    let opaque = JS_GetOpaque(global, JS_CLASS_OBJECT)
-    JS_FreeValue(ctx, global)
-    return ok(opaque)
-  JS_FreeValue(ctx, global)
-  return err()
-
-func getGlobalOpaque*(ctx: JSContext, T: typedesc, val: JSValue = JS_UNDEFINED): Opt[T] =
-  return ok(cast[T](?getGlobalOpaque0(ctx, val)))
-
-func getOpaque*(ctx: JSContext, val: JSValue, class: string): pointer =
-  # Unfortunately, we can't change the global object's class.
-  #TODO: or maybe we can, but I'm afraid of breaking something.
-  # This needs further investigation.
-  if ctx.isGlobal(class):
-    let global = JS_GetGlobalObject(ctx)
-    let opaque = JS_GetOpaque(global, JS_CLASS_OBJECT)
-    JS_FreeValue(ctx, global)
-    return opaque
-  return getOpaque0(val)
-
-func getOpaque*[T: ref object](ctx: JSContext, val: JSValue): T =
-  cast[T](getOpaque(ctx, val, $T))
-
 proc setInterruptHandler*(rt: JSRuntime, cb: JSInterruptHandler, opaque: pointer = nil) =
   JS_SetInterruptHandler(rt, cb, opaque)
 
-func toString*(ctx: JSContext, val: JSValue): Opt[string] =
-  var plen: csize_t
-  let outp = JS_ToCStringLen(ctx, addr plen, val) # cstring
-  if outp != nil:
-    var ret = newString(plen)
-    if plen != 0:
-      prepareMutation(ret)
-      copyMem(addr ret[0], outp, plen)
-    result = ok(ret)
-    JS_FreeCString(ctx, outp)
-
 proc writeException*(ctx: JSContext, s: Stream) =
   let ex = JS_GetException(ctx)
-  let str = toString(ctx, ex)
+  let str = fromJS[string](ctx, ex)
   if str.issome:
     s.write(str.get & '\n')
   let stack = JS_GetPropertyStr(ctx, ex, cstring("stack"));
   if not JS_IsUndefined(stack):
-    let str = toString(ctx, stack)
-    if str.issome:
+    let str = fromJS[string](ctx, stack)
+    if str.isSome:
       s.write(str.get)
   s.flush()
   JS_FreeValue(ctx, stack)
@@ -234,23 +188,6 @@ proc runJSJobs*(rt: JSRuntime, err: Stream) =
     if r == -1:
       ctx.writeException(err)
 
-func isInstanceOf*(ctx: JSContext, val: JSValue, class: string): bool =
-  let ctxOpaque = ctx.getOpaque()
-  var classid = JS_GetClassID(val)
-  let tclassid = ctxOpaque.creg[class]
-  var found = false
-  while true:
-    if classid == tclassid:
-      found = true
-      break
-    ctxOpaque.parents.withValue(classid, val):
-      classid = val[]
-    do:
-      classid = 0 # not defined by Chawan; assume parent is Object.
-    if classid == 0:
-      break
-  return found
-
 # Add all LegacyUnforgeable functions defined on the prototype chain to
 # the opaque.
 # Since every prototype has a list of all its ancestor's LegacyUnforgeable
@@ -343,428 +280,6 @@ func getMinArgs(params: seq[FuncParam]): int =
         return i
   return params.len
 
-func fromJSString(ctx: JSContext, val: JSValue): JSResult[string] =
-  var plen: csize_t
-  let outp = JS_ToCStringLen(ctx, addr plen, val) # cstring
-  if outp == nil:
-    return err()
-  var ret = newString(plen)
-  if plen != 0:
-    prepareMutation(ret)
-    copyMem(addr ret[0], outp, plen)
-  JS_FreeCString(ctx, outp)
-  return ok(ret)
-
-func fromJSInt[T: SomeInteger](ctx: JSContext, val: JSValue):
-    JSResult[T] =
-  if not JS_IsNumber(val):
-    return err()
-  when T is int:
-    # Always int32, so we don't risk 32-bit only breakage.
-    # If int64 is needed, specify it explicitly.
-    var ret: int32
-    if JS_ToInt32(ctx, addr ret, val) < 0:
-      return err()
-    return ok(int(ret))
-  elif T is uint:
-    var ret: uint32
-    if JS_ToUint32(ctx, addr ret, val) < 0:
-      return err()
-    return ok(uint(ret))
-  elif T is int32:
-    var ret: int32
-    if JS_ToInt32(ctx, addr ret, val) < 0:
-      return err()
-    return ok(ret)
-  elif T is int64:
-    var ret: int64
-    if JS_ToInt64(ctx, addr ret, val) < 0:
-      return err()
-    return ok(ret)
-  elif T is uint32:
-    var ret: uint32
-    if JS_ToUint32(ctx, addr ret, val) < 0:
-      return err()
-    return ok(ret)
-  elif T is uint64:
-    var ret: uint32
-    if JS_ToUint32(ctx, addr ret, val) < 0:
-      return err()
-    return ok(cast[uint64](ret))
-
-proc fromJSFloat[T: SomeFloat](ctx: JSContext, val: JSValue):
-    JSResult[T] =
-  if not JS_IsNumber(val):
-    return err()
-  var f64: float64
-  if JS_ToFloat64(ctx, addr f64, val) < 0:
-    return err()
-  return ok(cast[T](f64))
-
-proc fromJS*[T](ctx: JSContext, val: JSValue): JSResult[T]
-
-macro len(t: type tuple): int =
-  let i = t.getType()[1].len - 1 # - tuple
-  newLit(i)
-
-macro fromJSTupleBody(a: tuple) =
-  let len = a.getType().len - 1
-  let done = ident("done")
-  result = newStmtList(quote do:
-    var `done`: bool)
-  for i in 0..<len:
-    result.add(quote do:
-      let next = JS_Call(ctx, next_method, it, 0, nil)
-      if JS_IsException(next):
-        return err()
-      defer: JS_FreeValue(ctx, next)
-      let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE])
-      if JS_IsException(doneVal):
-        return err()
-      defer: JS_FreeValue(ctx, doneVal)
-      `done` = ?fromJS[bool](ctx, doneVal)
-      if `done`:
-        JS_ThrowTypeError(ctx, "Too few arguments in sequence (got %d, expected %d)", `i`, `len`)
-        return err()
-      let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[VALUE])
-      if JS_IsException(valueVal):
-        return err()
-      defer: JS_FreeValue(ctx, valueVal)
-      let genericRes = fromJS[typeof(`a`[`i`])](ctx, valueVal)
-      if genericRes.isErr: # exception
-        return err()
-      `a`[`i`] = genericRes.get
-    )
-    if i == len - 1:
-      result.add(quote do:
-        let next = JS_Call(ctx, next_method, it, 0, nil)
-        if JS_IsException(next):
-          return err()
-        defer: JS_FreeValue(ctx, next)
-        let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE])
-        `done` = ?fromJS[bool](ctx, doneVal)
-        var i = `i`
-        # we're emulating a sequence, so we must query all remaining parameters too:
-        while not `done`:
-          inc i
-          let next = JS_Call(ctx, next_method, it, 0, nil)
-          if JS_IsException(next):
-            return err()
-          defer: JS_FreeValue(ctx, next)
-          let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE])
-          if JS_IsException(doneVal):
-            return err()
-          defer: JS_FreeValue(ctx, doneVal)
-          `done` = ?fromJS[bool](ctx, doneVal)
-          if `done`:
-            let msg = "Too many arguments in sequence (got " & $i &
-              ", expected " & $`len` & ")"
-            return err(newTypeError(msg))
-          JS_FreeValue(ctx, JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[VALUE]))
-      )
-
-proc fromJSTuple[T: tuple](ctx: JSContext, val: JSValue): JSResult[T] =
-  let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_refs[ITERATOR])
-  if JS_IsException(itprop):
-    return err()
-  defer: JS_FreeValue(ctx, itprop)
-  let it = JS_Call(ctx, itprop, val, 0, nil)
-  if JS_IsException(it):
-    return err()
-  defer: JS_FreeValue(ctx, it)
-  let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().str_refs[NEXT])
-  if JS_IsException(next_method):
-    return err()
-  defer: JS_FreeValue(ctx, next_method)
-  var x: T
-  fromJSTupleBody(x)
-  return ok(x)
-
-proc fromJSSeq[T](ctx: JSContext, val: JSValue): JSResult[seq[T]] =
-  let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_refs[ITERATOR])
-  if JS_IsException(itprop):
-    return err()
-  defer: JS_FreeValue(ctx, itprop)
-  let it = JS_Call(ctx, itprop, val, 0, nil)
-  if JS_IsException(it):
-    return err()
-  defer: JS_FreeValue(ctx, it)
-  let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().str_refs[NEXT])
-  if JS_IsException(next_method):
-    return err()
-  defer: JS_FreeValue(ctx, next_method)
-  var s = newSeq[T]()
-  while true:
-    let next = JS_Call(ctx, next_method, it, 0, nil)
-    if JS_IsException(next):
-      return err()
-    defer: JS_FreeValue(ctx, next)
-    let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE])
-    if JS_IsException(doneVal):
-      return err()
-    defer: JS_FreeValue(ctx, doneVal)
-    let done = ?fromJS[bool](ctx, doneVal)
-    if done:
-      break
-    let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[VALUE])
-    if JS_IsException(valueVal):
-      return err()
-    defer: JS_FreeValue(ctx, valueVal)
-    let genericRes = fromJS[typeof(s[0])](ctx, valueVal)
-    if genericRes.isnone: # exception
-      return err()
-    s.add(genericRes.get)
-  return ok(s)
-
-proc fromJSSet[T](ctx: JSContext, val: JSValue): Opt[set[T]] =
-  let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_refs[ITERATOR])
-  if JS_IsException(itprop):
-    return err()
-  defer: JS_FreeValue(ctx, itprop)
-  let it = JS_Call(ctx, itprop, val, 0, nil)
-  if JS_IsException(it):
-    return err()
-  defer: JS_FreeValue(ctx, it)
-  let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().str_refs[NEXT])
-  if JS_IsException(next_method):
-    return err()
-  defer: JS_FreeValue(ctx, next_method)
-  var s: set[T]
-  while true:
-    let next = JS_Call(ctx, next_method, it, 0, nil)
-    if JS_IsException(next):
-      return err()
-    defer: JS_FreeValue(ctx, next)
-    let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().done)
-    if JS_IsException(doneVal):
-      return err()
-    defer: JS_FreeValue(ctx, doneVal)
-    let done = ?fromJS[bool](ctx, doneVal)
-    if done:
-      break
-    let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().value)
-    if JS_IsException(valueVal):
-      return err()
-    defer: JS_FreeValue(ctx, valueVal)
-    let genericRes = ?fromJS[typeof(s.items)](ctx, valueVal)
-    s.incl(genericRes)
-  return ok(s)
-
-proc fromJSTable[A, B](ctx: JSContext, val: JSValue): JSResult[Table[A, B]] =
-  var ptab: ptr UncheckedArray[JSPropertyEnum]
-  var plen: uint32
-  let flags = cint(JS_GPN_STRING_MASK)
-  if JS_GetOwnPropertyNames(ctx, addr ptab, addr plen, val, flags) == -1:
-    # exception
-    return err()
-  defer:
-    for i in 0 ..< plen:
-      JS_FreeAtom(ctx, ptab[i].atom)
-    js_free(ctx, ptab)
-  var res = Table[A, B]()
-  for i in 0 ..< plen:
-    let atom = ptab[i].atom
-    let k = JS_AtomToValue(ctx, atom)
-    defer: JS_FreeValue(ctx, k)
-    let kn = ?fromJS[A](ctx, k)
-    let v = JS_GetProperty(ctx, val, atom)
-    defer: JS_FreeValue(ctx, v)
-    let vn = ?fromJS[B](ctx, v)
-    res[kn] = vn
-  return ok(res)
-
-#TODO varargs
-proc fromJSFunction1[T, U](ctx: JSContext, val: JSValue):
-    proc(x: U): JSResult[T] =
-  return proc(x: U): JSResult[T] =
-    var arg1 = toJS(ctx, x)
-    #TODO exceptions?
-    let ret = JS_Call(ctx, val, JS_UNDEFINED, 1, addr arg1)
-    when T isnot void:
-      result = fromJS[T](ctx, ret)
-    JS_FreeValue(ctx, ret)
-
-proc isErrType(rt: NimNode): bool =
-  let rtType = rt[0]
-  let errType = getTypeInst(Err)
-  return errType.sameType(rtType) and rtType.sameType(errType)
-
-# unpack brackets
-proc getRealTypeFun(x: NimNode): NimNode =
-  var x = x.getTypeImpl()
-  while true:
-    if x.kind == nnkBracketExpr and x.len == 2:
-      x = x[1].getTypeImpl()
-      continue
-    break
-  return x
-
-macro unpackReturnType(f: typed) =
-  var x = f.getRealTypeFun()
-  let params = x.findChild(it.kind == nnkFormalParams)
-  let rv = params[0]
-  if rv.isErrType():
-    return quote do: void
-  let rvv = rv[1]
-  return quote do: `rvv`
-
-macro unpackArg0(f: typed) =
-  var x = f.getRealTypeFun()
-  let params = x.findChild(it.kind == nnkFormalParams)
-  let rv = params[1]
-  doAssert rv.kind == nnkIdentDefs
-  let rvv = rv[1]
-  return quote do: `rvv`
-
-proc fromJSFunction[T](ctx: JSContext, val: JSValue):
-    JSResult[T] =
-  #TODO all args...
-  return ok(
-    fromJSFunction1[
-      typeof(unpackReturnType(T)),
-      typeof(unpackArg0(T))
-    ](ctx, val))
-
-proc fromJSChar(ctx: JSContext, val: JSValue): Opt[char] =
-  let s = ?toString(ctx, val)
-  if s.len > 1:
-    return err()
-  return ok(s[0])
-
-proc fromJSRune(ctx: JSContext, val: JSValue): Opt[Rune] =
-  let s = ?toString(ctx, val)
-  var i = 0
-  var r: Rune
-  fastRuneAt(s, i, r)
-  if i < s.len:
-    return err()
-  return ok(r)
-
-template optionType[T](o: type Option[T]): auto =
-  T
-
-# wrap
-proc fromJSOption[T](ctx: JSContext, val: JSValue): JSResult[Option[T]] =
-  if JS_IsUndefined(val):
-    #TODO what about null?
-    return err()
-  let res = ?fromJS[T](ctx, val)
-  return ok(some(res))
-
-# wrap
-proc fromJSOpt[T](ctx: JSContext, val: JSValue): JSResult[T] =
-  if JS_IsUndefined(val):
-    #TODO what about null?
-    return err()
-  let res = fromJS[T.valType](ctx, val)
-  if res.isErr:
-    return ok(opt(T.valType))
-  return ok(opt(res.get))
-
-proc fromJSBool(ctx: JSContext, val: JSValue): JSResult[bool] =
-  let ret = JS_ToBool(ctx, val)
-  if ret == -1: # exception
-    return err()
-  if ret == 0:
-    return ok(false)
-  return ok(true)
-
-proc fromJSEnum[T: enum](ctx: JSContext, val: JSValue): JSResult[T] =
-  if JS_IsException(val):
-    return err()
-  let s = ?toString(ctx, val)
-  try:
-    return ok(parseEnum[T](s))
-  except ValueError:
-    return err(newTypeError("`" & s &
-      "' is not a valid value for enumeration " & $T))
-
-proc fromJSPObj0(ctx: JSContext, val: JSValue, t: string):
-    JSResult[pointer] =
-  if JS_IsException(val):
-    return err(nil)
-  if JS_IsNull(val):
-    return ok(nil)
-  let ctxOpaque = ctx.getOpaque()
-  if ctxOpaque.gclaz == t:
-    return ok(?getGlobalOpaque0(ctx, val))
-  if not JS_IsObject(val):
-    return err(newTypeError("Value is not an object"))
-  if not isInstanceOf(ctx, val, t):
-    let errmsg = t & " expected"
-    JS_ThrowTypeError(ctx, cstring(errmsg))
-    return err(newTypeError(errmsg))
-  let classid = JS_GetClassID(val)
-  let op = JS_GetOpaque(val, classid)
-  return ok(op)
-
-proc fromJSObject[T: ref object](ctx: JSContext, val: JSValue): JSResult[T] =
-  return ok(cast[T](?fromJSPObj0(ctx, val, $T)))
-
-type FromJSAllowedT = (object and not (Result|Option|Table|JSValue))
-
-proc fromJS*[T](ctx: JSContext, val: JSValue): JSResult[T] =
-  when T is string:
-    return fromJSString(ctx, val)
-  elif T is char:
-    return fromJSChar(ctx, val)
-  elif T is Rune:
-    return fromJSRune(ctx, val)
-  elif T is (proc):
-    return fromJSFunction[T](ctx, val)
-  elif T is Option:
-    return fromJSOption[optionType(T)](ctx, val)
-  elif T is Opt: # unwrap
-    return fromJSOpt[T](ctx, val)
-  elif T is seq:
-    return fromJSSeq[typeof(result.get.items)](ctx, val)
-  elif T is set:
-    return fromJSSet[typeof(result.get.items)](ctx, val)
-  elif T is tuple:
-    return fromJSTuple[T](ctx, val)
-  elif T is bool:
-    return fromJSBool(ctx, val)
-  elif typeof(result).valType is Table:
-    return fromJSTable[typeof(result.get.keys),
-      typeof(result.get.values)](ctx, val)
-  elif T is SomeInteger:
-    return fromJSInt[T](ctx, val)
-  elif T is SomeFloat:
-    return fromJSFloat[T](ctx, val)
-  elif T is enum:
-    return fromJSEnum[T](ctx, val)
-  elif T is JSValue:
-    return ok(val)
-  elif T is ref object:
-    return fromJSObject[T](ctx, val)
-  elif compiles(fromJS2(ctx, val, result)):
-    fromJS2(ctx, val, result)
-  else:
-    static:
-      error("Unrecognized type " & $T)
-
-proc fromJSPObj[T](ctx: JSContext, val: JSValue): JSResult[ptr T] =
-  return cast[JSResult[ptr T]](fromJSPObj0(ctx, val, $T))
-
-template fromJSP*[T](ctx: JSContext, val: JSValue): untyped =
-  when T is FromJSAllowedT:
-    fromJSPObj[T](ctx, val)
-  else:
-    fromJS[T](ctx, val)
-
-const JS_ATOM_TAG_INT = cuint(1u32 shl 31)
-
-func JS_IsNumber(v: JSAtom): JS_BOOL =
-  return (cast[cuint](v) and JS_ATOM_TAG_INT) != 0
-
-func fromJS[T: string|uint32](ctx: JSContext, atom: JSAtom): Opt[T] =
-  when T is SomeNumber:
-    if JS_IsNumber(atom):
-      return ok(T(cast[uint32](atom) and (not JS_ATOM_TAG_INT)))
-  else:
-    let val = JS_AtomToValue(ctx, atom)
-    return toString(ctx, val)
-
 func fromJSP[T: string|uint32](ctx: JSContext, atom: JSAtom): Opt[T] =
   return fromJS[T](ctx, atom)
 
diff --git a/src/js/opaque.nim b/src/js/opaque.nim
index 882de1d7..8e6c2d5d 100644
--- a/src/js/opaque.nim
+++ b/src/js/opaque.nim
@@ -2,6 +2,7 @@ import tables
 
 import bindings/quickjs
 import js/error
+import utils/opt
 
 type
   JSSymbolRefs* = enum
@@ -74,9 +75,45 @@ func getOpaque*(ctx: JSContext): JSContextOpaque =
 func getOpaque*(rt: JSRuntime): JSRuntimeOpaque =
   return cast[JSRuntimeOpaque](JS_GetRuntimeOpaque(rt))
 
+func isGlobal*(ctx: JSContext, class: string): bool =
+  assert class != ""
+  return ctx.getOpaque().gclaz == class
+
 proc setOpaque*[T](ctx: JSContext, val: JSValue, opaque: T) =
   let rt = JS_GetRuntime(ctx)
   let rtOpaque = rt.getOpaque()
   let p = JS_VALUE_GET_PTR(val)
   rtOpaque.plist[cast[pointer](opaque)] = p
   JS_SetOpaque(val, cast[pointer](opaque))
+
+# getOpaque, but doesn't work for global objects.
+func getOpaque0*(val: JSValue): pointer =
+  if JS_VALUE_GET_TAG(val) == JS_TAG_OBJECT:
+    return JS_GetOpaque(val, JS_GetClassID(val))
+
+func getGlobalOpaque0*(ctx: JSContext, val: JSValue = JS_UNDEFINED):
+    Opt[pointer] =
+  let global = JS_GetGlobalObject(ctx)
+  if JS_IsUndefined(val) or val == global:
+    let opaque = JS_GetOpaque(global, JS_CLASS_OBJECT)
+    JS_FreeValue(ctx, global)
+    return ok(opaque)
+  JS_FreeValue(ctx, global)
+  return err()
+
+func getGlobalOpaque*(ctx: JSContext, T: typedesc, val: JSValue = JS_UNDEFINED): Opt[T] =
+  return ok(cast[T](?getGlobalOpaque0(ctx, val)))
+
+func getOpaque*(ctx: JSContext, val: JSValue, class: string): pointer =
+  # Unfortunately, we can't change the global object's class.
+  #TODO: or maybe we can, but I'm afraid of breaking something.
+  # This needs further investigation.
+  if ctx.isGlobal(class):
+    let global = JS_GetGlobalObject(ctx)
+    let opaque = JS_GetOpaque(global, JS_CLASS_OBJECT)
+    JS_FreeValue(ctx, global)
+    return opaque
+  return getOpaque0(val)
+
+func getOpaque*[T: ref object](ctx: JSContext, val: JSValue): T =
+  cast[T](getOpaque(ctx, val, $T))
diff --git a/src/types/blob.nim b/src/types/blob.nim
index 8bc96db7..0c4b30b9 100644
--- a/src/types/blob.nim
+++ b/src/types/blob.nim
@@ -1,5 +1,6 @@
 import options
 
+import js/fromjs
 import js/javascript
 import utils/mimeguess
 import utils/twtstr
diff --git a/src/types/color.nim b/src/types/color.nim
index b89eef7a..8f01309f 100644
--- a/src/types/color.nim
+++ b/src/types/color.nim
@@ -4,6 +4,7 @@ import tables
 
 import bindings/quickjs
 import js/error
+import js/fromjs
 import js/javascript
 import js/tojs
 import utils/twtstr