about summary refs log blame commit diff stats
path: root/src/js/fromjs.nim
blob: 823a045ee004953b762c9b380621a99872250a56 (plain) (tree)
1
2
3
4
5
6
7
8
9

                  

                  

                       
                 
               
                 

                
                


                                                          
                                                                               















                                                                   





















                                                                              






















                                                                   


                                                                      

                                                             
































                                                          
                                                                     


                                          
                

















                                                                             


                                                                           





















































































                                                                                       
                                     






































                                                                             

                                               
























                                                                         
                          
                                                             


                                  
                                                              
                                




































                                                              

                                                 





                                  








                                                                         
                        






















                                                                     













                                                                               






                                                          
                                    


                                                      
                                            









                                                                             





                                                               











                                                                             


























                                                                               





















                                                                                

                                                                          



                                                            

                                  


















                                                        

                                  



                                  

                                       

                                    

                               

                                  



                                          


























                                                                      
import std/macros
import std/options
import std/tables
import std/unicode

import bindings/quickjs
import io/promise
import js/error
import js/jstypes
import js/opaque
import js/tojs
import types/opt

proc fromJS*[T](ctx: JSContext, val: JSValue): JSResult[T]

func isInstanceOfNonGlobal(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 isInstanceOfGlobal(ctx: JSContext, val: JSValue, class: string): bool =
  let ctxOpaque = ctx.getOpaque()
  #TODO gparent only works for a single level. (But this is not really a
  # problem right now, because our global objects have at most one inheritance
  # level.)
  if ctx.isGlobal(class) or ctxOpaque.creg[class] == ctxOpaque.gparent:
    # undefined -> global
    if JS_IsUndefined(val):
      return true
    if JS_IsObject(val):
      let global = JS_GetGlobalObject(ctx)
      let p0 = JS_VALUE_GET_PTR(global)
      let p1 = JS_VALUE_GET_PTR(val)
      JS_FreeValue(ctx, global)
      if p0 == p1:
        return true
  return false

func isInstanceOf*(ctx: JSContext, val: JSValue, class: string): bool =
  return ctx.isInstanceOfGlobal(val, class) or
    ctx.isInstanceOfNonGlobal(val, class)

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 fromJSString2(ctx: JSContext, val: JSValue): JSResult[JSString] =
  return ok(JS_VALUE_GET_STRING(val))

func fromJSInt[T: SomeInteger](ctx: JSContext, val: JSValue):
    JSResult[T] =
  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 fromJSFloat64(ctx: JSContext, val: JSValue): JSResult[float64] =
  var f64: float64
  if JS_ToFloat64(ctx, addr f64, val) < 0:
    return err()
  return ok(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)", cint(`i`),
          cint(`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]] =
  if not JS_IsObject(val):
    return err(newTypeError("object expected"))
  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] =
  #TODO this leaks memory!
  let dupval = JS_DupValue(ctx, JS_DupValue(ctx, val)) # save
  return proc(x: U): JSResult[T] =
    var arg1 = toJS(ctx, x)
    #TODO exceptions?
    let ret = JS_Call(ctx, dupval, JS_UNDEFINED, 1, addr arg1)
    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...
  if not JS_IsFunction(ctx, val):
    return err(newTypeError("function expected"))
  return ok(
    fromJSFunction1[
      typeof(unpackReturnType(T)),
      typeof(unpackArg0(T))
    ](ctx, val))

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(option(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)
  # cmp when len is small enough, otherwise hashmap
  when {T.low..T.high}.len <= 4:
    for e in T.low .. T.high:
      if $e == s:
        return ok(e)
  else:
    const tab = (func(): Table[string, T] =
      result = initTable[string, T]()
      for e in T.low .. T.high:
        result[$e] = e
    )()
    if s in tab:
      return ok(tab[s])
  return errTypeError("`" & 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)
  if ctx.isInstanceOfGlobal(val, t):
    return ok(?getGlobalOpaque0(ctx, val))
  if not JS_IsObject(val):
    return err(newTypeError("Value is not an object"))
  if not isInstanceOfNonGlobal(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)))

proc fromJSVoid(ctx: JSContext, val: JSValue): JSResult[void] =
  if JS_IsException(val):
    #TODO maybe wrap or something
    return err()
  return ok()

proc fromJSDict[T: JSDict](ctx: JSContext, val: JSValue): JSResult[T] =
  if not JS_IsUndefined(val) and not JS_IsNull(val) and not JS_IsObject(val):
    return err(newTypeError("Dictionary is not an object"))
  #TODO throw on missing required values
  var d: T
  if JS_IsObject(val):
    for k, v in d.fieldPairs:
      let esm = JS_GetPropertyStr(ctx, val, k)
      if not JS_IsUndefined(esm):
        v = ?fromJS[typeof(v)](ctx, esm)
  return ok(d)

proc fromJSArrayBuffer(ctx: JSContext, val: JSValue): JSResult[JSArrayBuffer] =
  var len: csize_t
  let p = JS_GetArrayBuffer(ctx, addr len, val)
  if p == nil:
    return err()
  let abuf = JSArrayBuffer(
    len: len,
    p: cast[ptr UncheckedArray[uint8]](p)
  )
  return ok(abuf)

proc fromJSArrayBufferView(ctx: JSContext, val: JSValue):
    JSResult[JSArrayBufferView] =
  var offset: csize_t
  var nmemb: csize_t
  var nsize: csize_t
  let jsbuf = JS_GetTypedArrayBuffer(ctx, val, addr offset, addr nmemb,
    addr nsize)
  let abuf = ?fromJSArrayBuffer(ctx, jsbuf)
  let view = JSArrayBufferView(
    abuf: abuf,
    offset: offset,
    nmemb: nmemb,
    nsize: nsize
  )
  return ok(view)

proc promiseThenCallback(ctx: JSContext, this_val: JSValue, argc: cint,
    argv: ptr JSValue, magic: cint, func_data: ptr JSValue): JSValue {.cdecl.} =
  let op = JS_GetOpaque(func_data[], JS_GetClassID(func_data[]))
  let p = cast[EmptyPromise](op)
  p.resolve()
  GC_unref(p)
  return JS_UNDEFINED

proc fromJSEmptyPromise(ctx: JSContext, val: JSValue): JSResult[EmptyPromise] =
  if not JS_IsObject(val):
    return err(newTypeError("Value is not an object"))
  #TODO I have a feeling this leaks memory in some cases :(
  var p = EmptyPromise()
  GC_ref(p)
  var tmp = JS_NewObject(ctx)
  JS_SetOpaque(tmp, cast[pointer](p))
  var fun = JS_NewCFunctionData(ctx, promiseThenCallback, 0, 0, 1, addr tmp)
  let res = JS_Invoke(ctx, val, ctx.getOpaque().str_refs[THEN], 1, addr fun)
  if JS_IsException(res):
    return err()
  return ok(p)

type FromJSAllowedT = (object and not (Result|Option|Table|JSValue|JSDict|
  JSArrayBuffer|JSArrayBufferView|JSUint8Array))

proc fromJS*[T](ctx: JSContext, val: JSValue): JSResult[T] =
  when T is string:
    return fromJSString(ctx, val)
  elif T is JSString:
    return fromJSString2(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 float64:
    return fromJSFloat64(ctx, val)
  elif T is enum:
    return fromJSEnum[T](ctx, val)
  elif T is JSValue:
    return ok(val)
  elif T is EmptyPromise:
    return fromJSEmptyPromise(ctx, val)
  elif T is ref object:
    return fromJSObject[T](ctx, val)
  elif T is void:
    return fromJSVoid(ctx, val)
  elif T is JSDict:
    return fromJSDict[T](ctx, val)
  elif T is JSArrayBuffer:
    return fromJSArrayBuffer(ctx, val)
  elif T is JSArrayBufferView:
    return fromJSArrayBufferView(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)