about summary refs log blame commit diff stats
path: root/src/js/tojs.nim
blob: 4090757d850a3f74e2c2bcceef320b84270bab29 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15














                                                                                
                                                          
























                                                                                



                       
                 
                 

                 
                


                                                         


























                                                                         







                                                                              

                                                                           
 
                                                                             
                               

                                                                              
 


                                                                        
                                                                                
                    
                                                                                




                    
                 
 
                             

                                                                


                                                                  
                                                                  


                                                            

                                                                


                                                                           
                                                                  


                                                            
                                                                             


                                                         
                                                                     


                                                           
                                                                      



                                                               
                                                                       


                                                      
                                                                         




                                   

                                                                         



                            
                                                 

                             
                                                

                              
                                              

                      
                                               

                            
                                               


                                                      
                                             

                            
                                                

                                     
                                                

                             
                                                


                                       
                                                 
                              
 
                                              

                           
                                                           





                                       
                                                  



                             
                                                             






                               

                                                  
                       
 
                                            










                                                         
                                                   






                             

                                                                       

                      
 
                                              












                                                         
                                                          
                                  


                                                           
                                 



                                                    
                          



                                                                              
                 
                                          
                               

              
                                                           


                            
                          
                      
                                                      


               
 
                                                      

                             
                                                                        









                                              
                                                                               












                                        
                                             

                      
                                                

          
                                                           
                                        
                                                                                


                               
                                                                    




                                          
                                                            
                                        
                                                                                


                               


                                                               





                                          
                                                                          
                                        
                                                                                


                                      
                



                             

                                                                 






                             

                                                                 





                                          
                                                   

                       




                                  

                                                                  

                        
 
                                                          

                                                                           
                                                        


                                                                     


                           
                                                       

                                                                         
                                                   






                                 
                                                                            







                                                       
                            
                      
                                                      


                  
 
                                                                            








                                                                         
# Automatic conversion of Nim types to JavaScript types.
#
# Every conversion involves copying unless explicitly noted below.
#
# * Primitives are converted to their respective JavaScript counterparts.
# * seq is converted to a JS array. Note: this always copies the seq's contents.
# * Promise is converted to a JS promise which will be resolved when the Nim
#   promise is resolved.
# * enum is converted to its stringifier's output.
# * JSValue is returned as-is, *without* a DupValue operation.
# * JSError is converted to a new error object corresponding to the error
#   it represents.
# * JSArrayBuffer, JSUint8Array are converted to a JS object without copying
#   their contents.
# * NarrowString is converted to a JS narrow string (with copying). For more
#   information on JS string handling, see js/jstypes.nim.
# * Finally, ref object is converted to a JS object whose opaque is the ref
#   object. (See below.)
#
# Note that ref objects can be seamlessly converted to JS objects, despite
# the fact that they are managed by two separate garbage collectors. This
# works thanks to a patch in QJS and machine oil. Basically:
#
# * Nim objects registered with registerType can be paired with one (1)
#   JS object each.
# * This happens on-demand, whenever the Nim object has to be converted into JS.
# * Once the conversion happened, the JS object will be kept alive until the
#   Nim object is destroyed, so that JS properties on the JS object are not
#   lost during a re-conversion.
# * Similarly, the Nim object is kept alive so long as the JS object is alive.
# * The patched in can_destroy hook is used to synchronize reference counts
#   of the two objects; this way, no memory leak occurs.
#
# There are also toJSP variants of object converters. These work identically
# to ref object converters, except the reference count of the closest
# `ref object' ancestor is incremented/decremented when synchronizing refcounts
# with the JS object pair.

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

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

# Convert Nim types to the corresponding JavaScript type.
# This does not work with var objects.
proc toJS*(ctx: JSContext; s: string): JSValue
proc toJS*(ctx: JSContext; r: Rune): JSValue
proc toJS*(ctx: JSContext; n: int64): JSValue
proc toJS*(ctx: JSContext; n: int32): JSValue
proc toJS*(ctx: JSContext; n: int): JSValue
proc toJS*(ctx: JSContext; n: uint16): JSValue
proc toJS*(ctx: JSContext; n: uint32): JSValue
proc toJS*(ctx: JSContext; n: uint64): JSValue
proc toJS*(ctx: JSContext; n: float64): JSValue
proc toJS*(ctx: JSContext; b: bool): JSValue
proc toJS*[U, V](ctx: JSContext; t: Table[U, V]): JSValue
proc toJS*(ctx: JSContext; opt: Option): JSValue
proc toJS*[T, E](ctx: JSContext; opt: Result[T, E]): JSValue
proc toJS*(ctx: JSContext; s: seq): JSValue
proc toJS*[T](ctx: JSContext; s: set[T]): JSValue
proc toJS*(ctx: JSContext; t: tuple): JSValue
proc toJS*(ctx: JSContext; e: enum): JSValue
proc toJS*(ctx: JSContext; j: JSValue): JSValue
proc toJS*[T](ctx: JSContext; promise: Promise[T]): JSValue
proc toJS*[T, E](ctx: JSContext; promise: Promise[Result[T, E]]): JSValue
proc toJS*(ctx: JSContext; promise: EmptyPromise): JSValue
proc toJS*(ctx: JSContext; obj: ref object): JSValue
proc toJS*(ctx: JSContext; err: JSError): JSValue
proc toJS*(ctx: JSContext; abuf: JSArrayBuffer): JSValue
proc toJS*(ctx: JSContext; u8a: JSUint8Array): JSValue
proc toJS*(ctx: JSContext; ns: NarrowString): JSValue
proc toJS*(ctx: JSContext; dict: JSDict): JSValue

# Convert Nim types to the corresponding JavaScript type, with knowledge of
# the parent object.
# This supports conversion of var object types.
#
# The idea here is to allow conversion of var objects to quasi-reference types
# by saving a pointer to their ancestor and incrementing/decrementing the
# ancestor's reference count instead.
proc toJSP*(ctx: JSContext; parent: ref object; child: var object): JSValue
proc toJSP*(ctx: JSContext; parent: ptr object; child: var object): JSValue

# Same as toJS, but used in constructors. ctor contains the target prototype,
# used for subclassing from JS.
proc toJSNew*(ctx: JSContext; obj: ref object; ctor: JSValue): JSValue
proc toJSNew*[T, E](ctx: JSContext; opt: Result[T, E]; ctor: JSValue): JSValue

# Avoid accidentally calling toJSP on objects that we have explicit toJS
# converters for.
template makeToJSP(typ: untyped) =
  template toJSP*(ctx: JSContext; parent: ref object; child: var typ): JSValue =
    toJS(ctx, child)
  template toJSP*(ctx: JSContext; parent: ptr object; child: var typ): JSValue =
    toJS(ctx, child)
makeToJSP(Table)
makeToJSP(Option)
makeToJSP(Result)
makeToJSP(JSValue)
makeToJSP(JSDict)

# Note: this consumes `prop'.
proc defineProperty(ctx: JSContext; this: JSValue; name: JSAtom;
    prop: JSValue; flags = cint(0)) =
  if JS_DefinePropertyValue(ctx, this, name, prop, flags) <= 0:
    raise newException(Defect, "Failed to define property string")

proc definePropertyC*(ctx: JSContext; this: JSValue; name: JSAtom;
    prop: JSValue) =
  ctx.defineProperty(this, name, prop, JS_PROP_CONFIGURABLE)

proc defineProperty(ctx: JSContext; this: JSValue; name: string;
    prop: JSValue; flags = cint(0)) =
  if JS_DefinePropertyValueStr(ctx, this, cstring(name), prop, flags) <= 0:
    raise newException(Defect, "Failed to define property string: " & name)

proc definePropertyC*(ctx: JSContext; this: JSValue; name: string;
    prop: JSValue) =
  ctx.defineProperty(this, name, prop, JS_PROP_CONFIGURABLE)

proc defineProperty*[T](ctx: JSContext; this: JSValue; name: string; prop: T;
    flags = cint(0)) =
  defineProperty(ctx, this, name, toJS(ctx, prop), flags)

proc definePropertyE*[T](ctx: JSContext; this: JSValue; name: string;
    prop: T) =
  defineProperty(ctx, this, name, prop, JS_PROP_ENUMERABLE)

proc definePropertyCW*[T](ctx: JSContext; this: JSValue; name: string;
    prop: T) =
  defineProperty(ctx, this, name, prop, JS_PROP_CONFIGURABLE or
    JS_PROP_WRITABLE)

proc definePropertyCWE*[T](ctx: JSContext; this: JSValue; name: string;
    prop: T) =
  defineProperty(ctx, this, name, prop, JS_PROP_C_W_E)

proc newFunction*(ctx: JSContext; args: openArray[string]; body: string):
    JSValue =
  var paramList: seq[JSValue] = @[]
  for arg in args:
    paramList.add(toJS(ctx, arg))
  paramList.add(toJS(ctx, body))
  let fun = JS_CallConstructor(ctx, ctx.getOpaque().valRefs[jsvFunction],
    cint(paramList.len), paramList.toJSValueArray())
  for param in paramList:
    JS_FreeValue(ctx, param)
  return fun

proc toJS*(ctx: JSContext; s: cstring): JSValue =
  return JS_NewString(ctx, s)

proc toJS*(ctx: JSContext; s: string): JSValue =
  return toJS(ctx, cstring(s))

proc toJS*(ctx: JSContext; r: Rune): JSValue =
  return toJS(ctx, $r)

proc toJS*(ctx: JSContext; n: int32): JSValue =
  return JS_NewInt32(ctx, n)

proc toJS*(ctx: JSContext; n: int64): JSValue =
  return JS_NewInt64(ctx, n)

# Always int32, so we don't risk 32-bit only breakage.
proc toJS*(ctx: JSContext; n: int): JSValue =
  return toJS(ctx, int32(n))

proc toJS*(ctx: JSContext; n: uint16): JSValue =
  return JS_NewUint32(ctx, uint32(n))

proc toJS*(ctx: JSContext; n: uint32): JSValue =
  return JS_NewUint32(ctx, n)

proc toJS*(ctx: JSContext; n: uint64): JSValue =
  #TODO this is incorrect
  return JS_NewFloat64(ctx, float64(n))

proc toJS*(ctx: JSContext; n: float64): JSValue =
  return JS_NewFloat64(ctx, n)

proc toJS*(ctx: JSContext; b: bool): JSValue =
  return JS_NewBool(ctx, b)

proc toJS*[U, V](ctx: JSContext; t: Table[U, V]): JSValue =
  let obj = JS_NewObject(ctx)
  if not JS_IsException(obj):
    for k, v in t:
      definePropertyCWE(ctx, obj, k, v)
  return obj

proc toJS*(ctx: JSContext; opt: Option): JSValue =
  if opt.isSome:
    return toJS(ctx, opt.get)
  return JS_NULL

proc toJS[T, E](ctx: JSContext; opt: Result[T, E]): JSValue =
  if opt.isSome:
    when not (T is void):
      return toJS(ctx, opt.get)
    else:
      return JS_UNDEFINED
  else:
    when not (E is void):
      if opt.error != nil:
        return JS_Throw(ctx, toJS(ctx, opt.error))
    return JS_EXCEPTION

proc toJS(ctx: JSContext; s: seq): JSValue =
  let a = JS_NewArray(ctx)
  if not JS_IsException(a):
    for i in 0..s.high:
      let j = toJS(ctx, s[i])
      if JS_IsException(j):
        return j
      if JS_DefinePropertyValueInt64(ctx, a, int64(i), j,
          JS_PROP_C_W_E or JS_PROP_THROW) < 0:
        return JS_EXCEPTION
  return a

proc toJS*[T](ctx: JSContext; s: set[T]): JSValue =
  #TODO this is a bit lazy :p
  var x = newSeq[T]()
  for e in s:
    x.add(e)
  var a = toJS(ctx, x)
  if JS_IsException(a):
    return a
  let ret = JS_CallConstructor(ctx, ctx.getOpaque().valRefs[jsvSet], 1,
    a.toJSValueArray())
  JS_FreeValue(ctx, a)
  return ret

proc toJS(ctx: JSContext; t: tuple): JSValue =
  let a = JS_NewArray(ctx)
  if not JS_IsException(a):
    var i = 0
    for f in t.fields:
      let j = toJS(ctx, f)
      if JS_IsException(j):
        return j
      if JS_DefinePropertyValueInt64(ctx, a, int64(i), j,
          JS_PROP_C_W_E or JS_PROP_THROW) < 0:
        return JS_EXCEPTION
      inc i
  return a

proc toJSP0(ctx: JSContext; p, tp: pointer; ctor: JSValue;
    needsref: var bool): JSValue =
  JS_GetRuntime(ctx).getOpaque().plist.withValue(p, obj):
    # a JSValue already points to this object.
    return JS_DupValue(ctx, JS_MKPTR(JS_TAG_OBJECT, obj[]))
  let ctxOpaque = ctx.getOpaque()
  let class = ctxOpaque.typemap[tp]
  let jsObj = JS_NewObjectFromCtor(ctx, ctor, class)
  if JS_IsException(jsObj):
    return jsObj
  setOpaque(ctx, jsObj, p)
  # We are constructing a new JS object, so we must add unforgeable properties
  # here.
  ctxOpaque.unforgeable.withValue(class, uf):
    JS_SetPropertyFunctionList(ctx, jsObj, addr uf[][0], cint(uf[].len))
  needsref = true
  if unlikely(ctxOpaque.htmldda == class):
    JS_SetIsHTMLDDA(ctx, jsObj)
  return jsObj

proc toJSRefObj(ctx: JSContext; obj: ref object): JSValue =
  if obj == nil:
    return JS_NULL
  let p = cast[pointer](obj)
  let tp = getTypePtr(obj)
  var needsref = false
  let val = toJSP0(ctx, p, tp, JS_UNDEFINED, needsref)
  if needsref:
    GC_ref(obj)
  return val

proc toJS*(ctx: JSContext; obj: ref object): JSValue =
  return toJSRefObj(ctx, obj)

proc toJSNew*(ctx: JSContext; obj: ref object; ctor: JSValue): JSValue =
  if obj == nil:
    return JS_NULL
  let p = cast[pointer](obj)
  let tp = getTypePtr(obj)
  var needsref = false
  let val = toJSP0(ctx, p, tp, ctor, needsref)
  if needsref:
    GC_ref(obj)
  return val

proc toJSNew[T, E](ctx: JSContext; opt: Result[T, E]; ctor: JSValue): JSValue =
  if opt.isSome:
    when not (T is void):
      return toJSNew(ctx, opt.get, ctor)
    else:
      return JS_UNDEFINED
  else:
    when not (E is void):
      let res = toJS(ctx, opt.error)
      if not JS_IsNull(res):
        return JS_Throw(ctx, res)
    else:
      return JS_NULL

proc toJS(ctx: JSContext; e: enum): JSValue =
  return toJS(ctx, $e)

proc toJS(ctx: JSContext; j: JSValue): JSValue =
  return j

proc toJS(ctx: JSContext; promise: EmptyPromise): JSValue =
  var resolving_funcs: array[2, JSValue]
  let jsPromise = JS_NewPromiseCapability(ctx, resolving_funcs.toJSValueArray())
  if JS_IsException(jsPromise):
    return JS_EXCEPTION
  promise.then(proc() =
    let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 0, nil)
    JS_FreeValue(ctx, res)
    JS_FreeValue(ctx, resolving_funcs[0])
    JS_FreeValue(ctx, resolving_funcs[1]))
  return jsPromise

proc toJS[T](ctx: JSContext; promise: Promise[T]): JSValue =
  var resolving_funcs: array[2, JSValue]
  let jsPromise = JS_NewPromiseCapability(ctx, resolving_funcs.toJSValueArray())
  if JS_IsException(jsPromise):
    return JS_EXCEPTION
  promise.then(proc(x: T) =
    let x = toJS(ctx, x)
    let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1,
      x.toJSValueArray())
    JS_FreeValue(ctx, res)
    JS_FreeValue(ctx, x)
    JS_FreeValue(ctx, resolving_funcs[0])
    JS_FreeValue(ctx, resolving_funcs[1]))
  return jsPromise

proc toJS[T, E](ctx: JSContext; promise: Promise[Result[T, E]]): JSValue =
  var resolving_funcs: array[2, JSValue]
  let jsPromise = JS_NewPromiseCapability(ctx, resolving_funcs.toJSValueArray())
  if JS_IsException(jsPromise):
    return JS_EXCEPTION
  promise.then(proc(x: Result[T, E]) =
    if x.isSome:
      let x = when T is void:
        JS_UNDEFINED
      else:
        toJS(ctx, x.get)
      let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1,
        x.toJSValueArray())
      JS_FreeValue(ctx, res)
      JS_FreeValue(ctx, x)
    else: # err
      let x = when E is void:
        JS_UNDEFINED
      else:
        toJS(ctx, x.error)
      let res = JS_Call(ctx, resolving_funcs[1], JS_UNDEFINED, 1,
        x.toJSValueArray())
      JS_FreeValue(ctx, res)
      JS_FreeValue(ctx, x)
    JS_FreeValue(ctx, resolving_funcs[0])
    JS_FreeValue(ctx, resolving_funcs[1]))
  return jsPromise

proc toJS*(ctx: JSContext; err: JSError): JSValue =
  if err == nil:
    return JS_EXCEPTION
  if err.e notin QuickJSErrors:
    return toJSRefObj(ctx, err)
  var msg = toJS(ctx, err.message)
  if JS_IsException(msg):
    return msg
  let ctor = ctx.getOpaque().errCtorRefs[err.e]
  let ret = JS_CallConstructor(ctx, ctor, 1, msg.toJSValueArray())
  JS_FreeValue(ctx, msg)
  return ret

proc toJS*(ctx: JSContext; abuf: JSArrayBuffer): JSValue =
  return JS_NewArrayBuffer(ctx, abuf.p, abuf.len, abuf.dealloc, nil, false)

proc toJS*(ctx: JSContext; u8a: JSUint8Array): JSValue =
  let jsabuf = toJS(ctx, u8a.abuf)
  let ctor = ctx.getOpaque().valRefs[jsvUint8Array]
  let ret = JS_CallConstructor(ctx, ctor, 1, jsabuf.toJSValueArray())
  JS_FreeValue(ctx, jsabuf)
  return ret

proc toJS*(ctx: JSContext; ns: NarrowString): JSValue =
  return JS_NewNarrowStringLen(ctx, cstring(ns), csize_t(string(ns).len))

proc toJS*(ctx: JSContext; dict: JSDict): JSValue =
  let obj = JS_NewObject(ctx)
  if JS_IsException(obj):
    return obj
  for k, v in dict.fieldPairs:
    ctx.defineProperty(obj, k, v)
  return obj

proc toJSP(ctx: JSContext; parent: ref object; child: var object): JSValue =
  let p = addr child
  # Save parent as the original ancestor for this tree.
  JS_GetRuntime(ctx).getOpaque().refmap[p] = (
    (proc() =
      GC_ref(parent)),
    (proc() =
      GC_unref(parent))
  )
  let tp = getTypePtr(child)
  var needsref = false
  let val = toJSP0(ctx, p, tp, JS_UNDEFINED, needsref)
  if needsref:
    GC_ref(parent)
  return val

proc toJSP(ctx: JSContext; parent: ptr object; child: var object): JSValue =
  let p = addr child
  # Increment the reference count of parent's root ancestor, and save the
  # increment/decrement callbacks for the child as well.
  let rtOpaque = JS_GetRuntime(ctx).getOpaque()
  let ru = rtOpaque.refmap[parent]
  ru.cref()
  rtOpaque.refmap[p] = ru
  let tp = getTypePtr(child)
  return toJSP0(ctx, p, tp)