## JavaScript binding generator. Horrifying, I know. But it works!
## Pragmas:
## {.jsctor.} for constructors. These need no `this' value, and are
## bound as regular constructors in JS. They must return a ref object,
## which will have a JS counterpart too. (Other functions can return
## ref objects too, which will either use the existing JS counterpart,
## if exists, or create a new one. In other words: cross-language
## reference semantics work seamlessly.)
## {.jsfunc.} is used for binding normal functions. Needs a `this'
## value, as all following pragmas. Generics are not supported, but
## JSValue does.
## By default, the Nim function name is bound; if this is not desired,
## you can rename the function like this: {.jsfunc: "preferredName".}
## This also works for all other pragmas that define named functions
## in JS.
## {.jsstfunc.} binds static functions. Unlike .jsfunc, it does not
## have a `this' value. A class name must be specified,
## e.g. {.jsstfunc: "URL".} to define on the URL class. To
## rename a static function, use the syntax "ClassName:funcName",
## e.g. "Response:error".
## {.jsget.}, {.jsfget.} must be specified on object fields; these
## generate regular getter & setter functions.
## {.jsufget, jsuffget, jsuffunc.} For fields with the
## [LegacyUnforgeable] WebIDL property.
## This makes it so a non-configurable/writable, but enumerable
## property is defined on the object when the *constructor* is called
## (i.e. NOT on the prototype.)
## {.jsrget.}, {.jsrfget.}: For fields with the [Replaceable] WebIDL
## property.
## {.jsfget.} and {.jsfset.} for getters/setters. Note the `f'; bare
## jsget/jsset can only be used on object fields. (I initially wanted
## to use the same keyword, unfortunately that didn't work out.)
## {.jsgetownprop.} Called when GetOwnProperty would return nothing. The
## key must be either a JSAtom, uint32 or string. (Note that the
## string option copies.)
## {.jsgetprop.} for property getters. Called on GetProperty.
## (In fact, this can be emulated using get_own_property, but this
## might still be faster.)
## {.jssetprop.} for property setters. Called on SetProperty - in fact
## this is the set() method of Proxy, except it always returns
## true. Same rules as jsgetprop for keys.
## {.jsdelprop.} for property deletion. It is like the deleteProperty
## method of Proxy. Must return true if deleted, false if not deleted.
## {.jshasprop.} for overriding has_property. Must return a boolean,
## or the integer 1 for true, 0 for false, or -1 for exception.
## {.jspropnames.} overrides get_own_property_names. Must return a
## JSPropertyEnumList object.
{.push raises: [].}
import std/macros
import std/options
import std/sets
import std/strutils
import std/tables
import fromjs
import jserror
import jsopaque
import optshim
import quickjs
import tojs
export options
export
JS_NULL, JS_UNDEFINED, JS_FALSE, JS_TRUE, JS_EXCEPTION, JS_UNINITIALIZED,
JS_EVAL_TYPE_GLOBAL,
JS_EVAL_TYPE_MODULE,
JS_EVAL_TYPE_DIRECT,
JS_EVAL_TYPE_INDIRECT,
JS_EVAL_TYPE_MASK,
JS_EVAL_FLAG_SHEBANG,
JS_EVAL_FLAG_STRICT,
JS_EVAL_FLAG_COMPILE_ONLY,
JSRuntime, JSContext, JSValue, JSValueConst, JSClassID, JSAtom,
JS_GetGlobalObject, JS_FreeValue, JS_IsException, JS_GetPropertyStr,
JS_IsFunction, JS_NewCFunctionData, JS_Call, JS_DupValue, JS_IsUndefined,
JS_ThrowTypeError, JS_ThrowRangeError, JS_ThrowSyntaxError,
JS_ThrowInternalError, JS_ThrowReferenceError
when sizeof(int) < sizeof(int64):
export quickjs.`==`
type
JSFunctionList = openArray[JSCFunctionListEntry]
BoundFunctionType = enum
bfFunction = "js_func"
bfConstructor = "js_ctor"
bfGetter = "js_get"
bfSetter = "js_set"
bfPropertyGetOwn = "js_prop_get_own"
bfPropertyGet = "js_prop_get"
bfPropertySet = "js_prop_set"
bfPropertyDel = "js_prop_del"
bfPropertyHas = "js_prop_has"
bfPropertyNames = "js_prop_names"
bfFinalizer = "js_fin"
BoundFunctionFlag = enum
bffNone, bffUnforgeable, bffStatic, bffReplaceable
BoundFunction = object
t: BoundFunctionType
flag: BoundFunctionFlag
magic: uint16
name: string
id: NimNode
var runtimes {.threadvar.}: seq[JSRuntime]
proc bindCalloc(s: pointer; count, size: csize_t): pointer {.cdecl.} =
let n = count * size
if n > size:
return nil
return alloc0(count * size)
proc bindMalloc(s: pointer; size: csize_t): pointer {.cdecl.} =
return alloc(size)
proc bindFree(s: pointer; p: pointer) {.cdecl.} =
if p != nil:
dealloc(p)
proc bindRealloc(s: pointer; p: pointer; size: csize_t): pointer {.cdecl.} =
return realloc(p, size)
proc jsRuntimeCleanUp(rt: JSRuntime) {.cdecl.} =
let rtOpaque = rt.getOpaque()
GC_unref(rtOpaque)
# For refc: ensure there are no ghost Nim objects holding onto JS
# values.
try:
GC_fullCollect()
except Exception:
quit(1)
JS_RunGC(rt)
assert rtOpaque.destroying == nil
var np = 0
for it in rtOpaque.plist.values:
rtOpaque.tmplist[np] = it.p
inc np
rtOpaque.plist.clear()
var nu = 0
for p in rtOpaque.parentMap.values:
rtOpaque.tmpunrefs[nu] = p
inc nu
rtOpaque.parentMap.clear()
for i in 0 ..< nu:
GC_unref(cast[RootRef](rtOpaque.tmpunrefs[i]))
for i in 0 ..< np:
let p = rtOpaque.tmplist[i]
let val = JS_MKPTR(JS_TAG_OBJECT, p)
let classid = JS_GetClassID(val)
let opaque = JS_GetOpaque(val, classid)
if opaque != nil:
let nimt = rtOpaque.inverseTypemap.getOrDefault(classid)
rtOpaque.fins.withValue(nimt, fin):
fin[](rt, opaque)
JS_SetOpaque(val, nil)
JS_FreeValueRT(rt, val)
# GC will run again now
proc newJSRuntime*(): JSRuntime =
## Instantiate a Monoucha `JSRuntime`.
var mf {.global.} = JSMallocFunctions(
js_calloc: bindCalloc,
js_malloc: bindMalloc,
js_free: bindFree,
js_realloc: bindRealloc,
js_malloc_usable_size: nil
)
let rt = JS_NewRuntime2(addr mf, nil)
let opaque = JSRuntimeOpaque()
GC_ref(opaque)
JS_SetRuntimeOpaque(rt, cast[pointer](opaque))
JS_SetRuntimeCleanUpFunc(rt, jsRuntimeCleanUp)
# Must be added after opaque is set, or there is a chance of
# nim_finalize_for_js dereferencing it (at the new call).
runtimes.add(rt)
return rt
proc newJSContext*(rt: JSRuntime): JSContext =
## Instantiate a Monoucha `JSContext`.
## It is only valid to call Monoucha procedures on contexts initialized with
## `newJSContext`, as it does extra initialization over `JS_NewContext`.
let ctx = JS_NewContext(rt)
let opaque = newJSContextOpaque(ctx)
GC_ref(opaque)
JS_SetContextOpaque(ctx, cast[pointer](opaque))
return ctx
proc free*(ctx: JSContext) =
## Free the JSContext and associated resources.
## Note: this is not an alias of `JS_FreeContext`; `free` also frees various
## JSValues stored on context startup by `newJSContext`.
var opaque = ctx.getOpaque()
if opaque != nil:
for a in opaque.symRefs:
JS_FreeAtom(ctx, a)
for a in opaque.strRefs:
JS_FreeAtom(ctx, a)
for v in opaque.valRefs:
JS_FreeValue(ctx, v)
for classid, v in opaque.ctors:
JS_FreeValue(ctx, v)
for e, v in opaque.errCtorRefs:
if e != jeCustom:
JS_FreeValue(ctx, v)
if opaque.globalUnref != nil:
opaque.globalUnref()
JS_FreeValue(ctx, opaque.global)
GC_unref(opaque)
JS_FreeContext(ctx)
proc free*(rt: JSRuntime) =
## Free the `JSRuntime` rt and remove it from the global JSRuntime pool.
#
# We must prepare space for opaque refs & pointers here, so that we
# can avoid allocations during cleanup. Otherwise we risk triggering a
# GC cycle and that would break cleanup too...
#
# (But we must *not* collect them yet; wait until the cycles are
# collected once.)
let rtOpaque = rt.getOpaque()
rtOpaque.tmplist.setLen(rtOpaque.plist.len)
rtOpaque.tmpunrefs.setLen(rtOpaque.parentMap.len)
JS_FreeRuntime(rt)
runtimes.del(runtimes.find(rt))
proc setGlobal*[T](ctx: JSContext; obj: T) =
## Set the global variable to the reference `obj`.
## Note: you must call `ctx.registerType(T, asglobal = true)` for this to
## work, `T` being the type of `obj`.
# Add JSValue reference.
let ctxOpaque = ctx.getOpaque()
let opaque = cast[pointer](obj)
ctx.setOpaque(ctxOpaque.global, opaque)
GC_ref(obj)
let rtOpaque = JS_GetRuntime(ctx).getOpaque()
ctx.getOpaque().globalUnref = proc() =
GC_unref(obj)
rtOpaque.plist.del(opaque)
proc getExceptionMsg*(ctx: JSContext): string =
result = ""
let ex = JS_GetException(ctx)
if fromJS(ctx, ex, result).isSome:
result &= '\n'
let stack = JS_GetPropertyStr(ctx, ex, cstring("stack"));
var s: string
if not JS_IsUndefined(stack) and ctx.fromJS(stack, s).isSome:
result &= s
JS_FreeValue(ctx, stack)
JS_FreeValue(ctx, ex)
# Returns early with err(JSContext) if an exception was thrown in a
# context.
proc runJSJobs*(rt: JSRuntime): Result[void, JSContext] =
while JS_IsJobPending(rt):
var ctx: JSContext
let r = JS_ExecutePendingJob(rt, ctx)
if r == -1:
return err(ctx)
ok()
# Add all LegacyUnforgeable functions defined on the prototype chain to
# the opaque.
# Since every prototype has a list of all its ancestor's LegacyUnforgeable
# functions, it is sufficient to simply merge the new list of new classes
# with their parent's list to achieve this.
proc addClassUnforgeable(ctx: JSContext; proto: JSValueConst;
classid, parent: JSClassID; ourUnforgeable: JSFunctionList) =
let ctxOpaque = ctx.getOpaque()
var merged = @ourUnforgeable
if int(parent) < ctxOpaque.unforgeable.len:
merged.add(ctxOpaque.unforgeable[int(parent)])
if merged.len > 0:
if int(classid) >= ctxOpaque.unforgeable.len:
ctxOpaque.unforgeable.setLen(int(classid) + 1)
ctxOpaque.unforgeable[int(classid)] = move(merged)
let ufp0 = addr ctxOpaque.unforgeable[int(classid)][0]
let ufp = cast[ptr UncheckedArray[JSCFunctionListEntry]](ufp0)
JS_SetPropertyFunctionList(ctx, proto, ufp, cint(merged.len))
proc newProtoFromParentClass(ctx: JSContext; parent: JSClassID): JSValue =
if parent != 0:
let parentProto = JS_GetClassProto(ctx, parent)
let proto = JS_NewObjectProtoClass(ctx, parentProto, parent)
JS_FreeValue(ctx, parentProto)
return proto
return JS_NewObject(ctx)
proc newCtorFunFromParentClass(ctx: JSContext; ctor: JSCFunction;
className: cstring; parent: JSClassID): JSValue =
if parent != 0:
return JS_NewCFunction3(ctx, ctor, className, 0, JS_CFUNC_constructor,
0, ctx.getOpaque().ctors[parent])
return JS_NewCFunction2(ctx, ctor, className, 0, JS_CFUNC_constructor, 0)
func newJSClass*(ctx: JSContext; cdef: JSClassDefConst; nimt: pointer;
ctor: JSCFunction; funcs: JSFunctionList; parent: JSClassID;
asglobal, nointerface: bool; finalizer: JSFinalizerFunction;
namespace: JSValueConst; unforgeable, staticfuns: JSFunctionList): JSClassID
{.discardable.} =
result = 0
let rt = JS_GetRuntime(ctx)
discard JS_NewClassID(rt, result)
let ctxOpaque = ctx.getOpaque()
let rtOpaque = rt.getOpaque()
if JS_NewClass(rt, result, cdef) != 0:
raise newException(Defect, "Failed to allocate JS class: " &
$cdef.class_name)
ctxOpaque.typemap[nimt] = result
rtOpaque.inverseTypemap[result] = nimt
if ctxOpaque.parents.len <= int(result):
ctxOpaque.parents.setLen(int(result) + 1)
ctxOpaque.parents[result] = parent
if finalizer != nil:
rtOpaque.fins[nimt] = finalizer
let proto = ctx.newProtoFromParentClass(parent)
if funcs.len > 0:
# We avoid funcs being GC'ed by putting the list in rtOpaque.
# (QuickJS uses the pointer later.)
#TODO maybe put them in ctxOpaque instead?
rtOpaque.flist.add(@funcs)
let fp0 = addr rtOpaque.flist[^1][0]
let fp = cast[ptr UncheckedArray[JSCFunctionListEntry]](fp0)
JS_SetPropertyFunctionList(ctx, proto, fp, cint(funcs.len))
#TODO check if this is an indexed property getter
if cdef.exotic != nil and cdef.exotic.get_own_property != nil:
let val = JS_DupValue(ctx, ctxOpaque.valRefs[jsvArrayPrototypeValues])
let itSym = ctxOpaque.symRefs[jsyIterator]
doAssert ctx.defineProperty(proto, itSym, val) == dprSuccess
let news = JS_NewAtomString(ctx, cdef.class_name)
doAssert not JS_IsException(news)
doAssert ctx.definePropertyC(proto, ctxOpaque.symRefs[jsyToStringTag],
JS_DupValue(ctx, news)) == dprSuccess
JS_SetClassProto(ctx, result, proto)
ctx.addClassUnforgeable(proto, result, parent, unforgeable)
if asglobal:
let global = ctxOpaque.global
assert ctxOpaque.gclass == 0
ctxOpaque.gclass = result
doAssert ctx.definePropertyC(global, ctxOpaque.symRefs[jsyToStringTag],
JS_DupValue(ctx, news)) == dprSuccess
if JS_SetPrototype(ctx, global, proto) != 1:
raise newException(Defect, "Failed to set global prototype: " &
$cdef.class_name)
# Global already exists, so set unforgeable functions here
if int(result) < ctxOpaque.unforgeable.len and
ctxOpaque.unforgeable[int(result)].len > 0:
let ufp0 = addr ctxOpaque.unforgeable[int(result)][0]
let ufp = cast[ptr UncheckedArray[JSCFunctionListEntry]](ufp0)
JS_SetPropertyFunctionList(ctx, global, ufp,
cint(ctxOpaque.unforgeable[int(result)].len))
JS_FreeValue(ctx, news)
let jctor = ctx.newCtorFunFromParentClass(ctor, cdef.class_name, parent)
if staticfuns.len > 0:
rtOpaque.flist.add(@staticfuns)
let fp0 = addr rtOpaque.flist[^1][0]
let fp = cast[ptr UncheckedArray[JSCFunctionListEntry]](fp0)
JS_SetPropertyFunctionList(ctx, jctor, fp, cint(staticfuns.len))
JS_SetConstructor(ctx, jctor, proto)
while ctxOpaque.ctors.len <= int(result):
ctxOpaque.ctors.add(JS_UNDEFINED)
ctxOpaque.ctors[result] = JS_DupValue(ctx, jctor)
if not nointerface:
let target = if JS_IsNull(namespace):
JSValueConst(ctxOpaque.global)
else:
namespace
doAssert JS_DefinePropertyValueStr(ctx, target, cdef.class_name, jctor,
JS_PROP_CONFIGURABLE or JS_PROP_WRITABLE) == 1
else:
JS_FreeValue(ctx, jctor)
type
FuncParam = tuple
t: NimNode
val: Option[NimNode]
JSFuncGenerator = object
t: BoundFunctionType
hasThis: bool
flag: BoundFunctionFlag
funcName: string
funcParams: seq[FuncParam]
thisType: string
thisTypeNode: NimNode
returnType: Option[NimNode]
newName: NimNode
dielabel: NimNode # die: jump to exception return code (JS_EXCEPTION or -1)
jsFunCallList: NimNode
jsFunCall: NimNode
jsCallAndRet: NimNode
minArgs: cint
actualMinArgs: cint # minArgs without JSContext
i: cint # nim parameters accounted for
j: cint # js parameters accounted for (not including fix ones, e.g. `this')
RegistryInfo = ref object
t: NimNode # NimNode of type
name: string # JS name, if this is the empty string then it equals tname
tabList: NimNode # array of function table
ctorFun: NimNode # constructor ident
getset: Table[string, (NimNode, NimNode, BoundFunctionFlag)] # name -> value
propGetOwnFun: NimNode # custom own get function ident
propGetFun: NimNode # custom get function ident
propSetFun: NimNode # custom set function ident
propDelFun: NimNode # custom del function ident
propHasFun: NimNode # custom has function ident
propNamesFun: NimNode # custom property names function ident
finFun: NimNode # finalizer ident
finName: NimNode # finalizer wrapper ident
dfin: NimNode # CheckDestroy finalizer ident
tabUnforgeable: NimNode # array of unforgeable function table
tabStatic: NimNode # array of static function table
replaceableSetFun: NimNode # replaceable setter function ident
tabReplaceableNames: NimNode # replaceable names array
markFun: NimNode # gc_mark for class
markList: NimNode # list of members to mark
var BoundFunctions {.compileTime.}: Table[string, RegistryInfo]
proc newRegistryInfo(t: NimNode): RegistryInfo =
return RegistryInfo(
t: t,
name: t.strVal,
tabList: newNimNode(nnkBracket),
tabUnforgeable: newNimNode(nnkBracket),
tabStatic: newNimNode(nnkBracket),
tabReplaceableNames: newNimNode(nnkBracket),
finName: newNilLit(),
finFun: newNilLit(),
propGetOwnFun: newNilLit(),
propGetFun: newNilLit(),
propSetFun: newNilLit(),
propDelFun: newNilLit(),
propHasFun: newNilLit(),
propNamesFun: newNilLit(),
markFun: newNilLit(),
markList: newStmtList()
)
proc readParams(gen: var JSFuncGenerator; fun: NimNode) =
let formalParams = fun.params
if formalParams[0].kind != nnkEmpty:
gen.returnType = some(formalParams[0])
var minArgsSeen = false
for i in 1 ..< formalParams.len:
let it = formalParams[i]
var val = it[^1]
if val.kind == nnkEmpty:
val = nil
var t = it[^2]
case t.kind
of nnkEmpty:
if val.kind == nnkEmpty:
error("?? " & treeRepr(val))
t = quote do:
typeof(`val`)
of nnkVarTy:
t = newNimNode(nnkPtrTy).add(t[0])
of nnkCommand, nnkCall:
if t.len == 2 and t[0].eqIdent("sink"):
t = t[1]
of nnkBracketExpr:
if t.typeKind == ntyVarargs:
if i != formalParams.len - 1:
error("varargs must be the last parameter")
minArgsSeen = true
else: discard
for i in 0 ..< it.len - 2:
gen.funcParams.add((t, option(val)))
if val != nil:
minArgsSeen = true
elif not minArgsSeen:
gen.minArgs = cint(gen.funcParams.len)
gen.actualMinArgs = gen.minArgs
if gen.hasThis and gen.flag != bffStatic:
dec gen.actualMinArgs
if gen.funcParams.len > gen.i:
if gen.funcParams[gen.i].t.eqIdent("JSContext"):
dec gen.actualMinArgs
gen.jsFunCall.add(ident("ctx"))
inc gen.i
elif gen.funcParams[gen.i].t.eqIdent("JSRuntime"):
inc gen.i # special case for finalizers that have a JSRuntime param
assert gen.actualMinArgs >= 0
template getJSParams(): untyped =
[
(quote do: JSValue),
newIdentDefs(ident("ctx"), quote do: JSContext),
newIdentDefs(ident("this"), quote do: JSValueConst),
newIdentDefs(ident("argc"), quote do: cint),
newIdentDefs(ident("argv"), quote do: JSValueConstArray)
]
template getJSGetterParams(): untyped =
[
(quote do: JSValue),
newIdentDefs(ident("ctx"), quote do: JSContext),
newIdentDefs(ident("this"), quote do: JSValueConst),
]
template getJSMagicGetterParams(): untyped =
[
(quote do: JSValue),
newIdentDefs(ident("ctx"), quote do: JSContext),
newIdentDefs(ident("this"), quote do: JSValueConst),
newIdentDefs(ident("magic"), quote do: cint)
]
template getJSGetOwnPropParams(): untyped =
[
(quote do: cint),
newIdentDefs(ident("ctx"), quote do: JSContext),
newIdentDefs(ident("desc"), quote do: ptr JSPropertyDescriptor),
newIdentDefs(ident("this"), quote do: JSValueConst),
newIdentDefs(ident("prop"), quote do: JSAtom),
]
template getJSGetPropParams(): untyped =
[
(quote do: JSValue),
newIdentDefs(ident("ctx"), quote do: JSContext),
newIdentDefs(ident("this"), quote do: JSValueConst),
newIdentDefs(ident("prop"), quote do: JSAtom),
newIdentDefs(ident("receiver"), quote do: JSValueConst),
]
template getJSSetPropParams(): untyped =
[
(quote do: cint),
newIdentDefs(ident("ctx"), quote do: JSContext),
newIdentDefs(ident("this"), quote do: JSValueConst),
newIdentDefs(ident("atom"), quote do: JSAtom),
newIdentDefs(ident("value"), quote do: JSValueConst),
newIdentDefs(ident("receiver"), quote do: JSValueConst),
newIdentDefs(ident("flags"), quote do: cint),
]
template getJSDelPropParams(): untyped =
[
(quote do: cint),
newIdentDefs(ident("ctx"), quote do: JSContext),
newIdentDefs(ident("this"), quote do: JSValueConst),
newIdentDefs(ident("prop"), quote do: JSAtom),
]
template getJSHasPropParams(): untyped =
[
(quote do: cint),
newIdentDefs(ident("ctx"), quote do: JSContext),
newIdentDefs(ident("this"), quote do: JSValueConst),
newIdentDefs(ident("atom"), quote do: JSAtom),
]
template getJSSetterParams(): untyped =
[
(quote do: JSValue),
newIdentDefs(ident("ctx"), quote do: JSContext),
newIdentDefs(ident("this"), quote do: JSValueConst),
newIdentDefs(ident("val"), quote do: JSValueConst),
]
template getJSPropNamesParams(): untyped =
[
(quote do: cint),
newIdentDefs(ident("ctx"), quote do: JSContext),
newIdentDefs(ident("ptab"), quote do: ptr JSPropertyEnumArray),
newIdentDefs(ident("plen"), quote do: ptr uint32),
newIdentDefs(ident("this"), quote do: JSValueConst)
]
proc addParam(gen: var JSFuncGenerator; s, t, val: NimNode;
fallback: NimNode = nil) =
if t.typeKind == ntyGenericParam:
error("Union parameters are no longer supported. Use JSValue instead.")
let dl = gen.dielabel
if fallback == nil:
gen.jsFunCallList.add(quote do:
var `s`: `t`
if ctx.fromJS(`val`, `s`).isNone:
break `dl`
)
else:
let j = gen.j
gen.jsFunCallList.add(quote do:
var `s`: `t`
if `j` < argc and not JS_IsUndefined(argv[`j`]):
if ctx.fromJS(`val`, `s`).isNone:
break `dl`
else:
`s` = `fallback`
)
proc addValueParam(gen: var JSFuncGenerator; s, t: NimNode;
fallback: NimNode = nil) =
let j = gen.j
gen.addParam(s, t, quote do: argv[`j`], fallback)
proc addThisParam(gen: var JSFuncGenerator; thisName = "this") =
var s = ident("arg_" & $gen.i)
let t = gen.funcParams[gen.i].t
let id = ident(thisName)
let dl = gen.dielabel
gen.jsFunCallList.add(quote do:
var `s`: `t`
if ctx.fromJSThis(`id`, `s`).isNone:
break `dl`
)
if gen.funcParams[gen.i].t.kind == nnkPtrTy:
s = quote do: `s`[]
gen.jsFunCall.add(s)
inc gen.i
proc addFixParam(gen: var JSFuncGenerator; name: string) =
var s = ident("arg_" & $gen.i)
let t = gen.funcParams[gen.i].t
let id = ident(name)
gen.addParam(s, t, id)
if gen.funcParams[gen.i].t.kind == nnkPtrTy:
s = quote do: `s`[]
gen.jsFunCall.add(s)
inc gen.i
proc addRequiredParams(gen: var JSFuncGenerator) =
while gen.i < gen.minArgs:
var s = ident("arg_" & $gen.i)
let tt = gen.funcParams[gen.i].t
gen.addValueParam(s, tt)
if gen.funcParams[gen.i].t.kind == nnkPtrTy:
s = quote do: `s`[]
gen.jsFunCall.add(s)
inc gen.j
inc gen.i
proc addOptionalParams(gen: var JSFuncGenerator) =
while gen.i < gen.funcParams.len:
let j = gen.j
var s = ident("arg_" & $gen.i)
let tt = gen.funcParams[gen.i].t
if tt.typeKind == varargs.getType().typeKind: # pray it's not a generic...
let vt = tt[1]
if vt.sameType(JSValueConst.getType()) or
JSValueConst.getType().sameType(vt):
s = quote do:
argv.toOpenArray(`j`, argc - 1)
else:
error("Only JSValueConst varargs are supported")
else:
let fallback = gen.funcParams[gen.i].val
if fallback.isNone:
error("No fallback value. Maybe a non-optional parameter follows an " &
"optional parameter?")
gen.addValueParam(s, tt, fallback.get)
if gen.funcParams[gen.i].t.kind == nnkPtrTy:
s = quote do: `s`[]
gen.jsFunCall.add(s)
inc gen.j
inc gen.i
proc finishFunCallList(gen: var JSFuncGenerator) =
gen.jsFunCallList.add(gen.jsFunCall)
var jsDtors {.compileTime.}: HashSet[string]
proc registerFunction(info: RegistryInfo; fun: BoundFunction) =
let f0 = fun.name
let f1 = fun.id
case fun.t
of bfFunction:
case fun.flag
of bffNone:
info.tabList.add(quote do:
JS_CFUNC_DEF(`f0`, 0, cast[JSCFunction](`f1`)))
of bffUnforgeable:
info.tabUnforgeable.add(quote do:
JS_CFUNC_DEF_NOCONF(`f0`, 0, cast[JSCFunction](`f1`)))
of bffStatic:
info.tabStatic.add(quote do:
JS_CFUNC_DEF(`f0`, 0, cast[JSCFunction](`f1`)))
of bffReplaceable:
assert false #TODO
of bfConstructor:
if info.ctorFun != nil:
error("Class " & info.name & " has 2+ constructors.")
info.ctorFun = f1
of bfGetter:
info.getset.withValue(f0, exv):
exv[0] = f1
exv[2] = fun.flag
do:
info.getset[f0] = (f1, newNilLit(), fun.flag)
if fun.flag == bffReplaceable:
info.tabReplaceableNames.add(newCall("cstring", newStrLitNode(f0)))
of bfSetter:
info.getset.withValue(f0, exv):
exv[1] = f1
do:
info.getset[f0] = (newNilLit(), f1, bffNone)
of bfPropertyGetOwn:
if info.propGetFun.kind != nnkNilLit:
error("Class " & info.name & " has 2+ own property getters.")
info.propGetOwnFun = f1
of bfPropertyGet:
if info.propGetFun.kind != nnkNilLit:
error("Class " & info.name & " has 2+ property getters.")
info.propGetFun = f1
of bfPropertySet:
if info.propSetFun.kind != nnkNilLit:
error("Class " & info.name & " has 2+ property setters.")
info.propSetFun = f1
of bfPropertyDel:
if info.propDelFun.kind != nnkNilLit:
error("Class " & info.name & " has 2+ property deleters.")
info.propDelFun = f1
of bfPropertyHas:
if info.propHasFun.kind != nnkNilLit:
error("Class " & info.name & " has 2+ hasprop getters.")
info.propHasFun = f1
of bfPropertyNames:
if info.propNamesFun.kind != nnkNilLit:
error("Class " & info.name & " has 2+ propnames getters.")
info.propNamesFun = f1
of bfFinalizer:
info.finFun = ident(f0)
info.finName = f1
proc registerFunction(typ: string; fun: BoundFunction) =
var info = BoundFunctions.getOrDefault(typ)
if info == nil:
info = newRegistryInfo(ident(typ))
BoundFunctions[typ] = info
info.registerFunction(fun)
proc registerFunction(gen: JSFuncGenerator) =
registerFunction(gen.thisType, BoundFunction(
t: gen.t,
name: gen.funcName,
id: gen.newName,
flag: gen.flag
))
proc jsCheckNumArgs(ctx: JSContext; argc, minargs: cint): bool =
if argc < minargs:
JS_ThrowTypeError(ctx, "At least %d arguments required, but only %d passed",
minargs, argc)
return false
true
proc newJSProc(gen: var JSFuncGenerator; params: openArray[NimNode];
isva = true): NimNode =
let ma = gen.actualMinArgs
let jsBody = newStmtList()
if isva and ma > 0:
jsBody.add(quote do:
if not ctx.jsCheckNumArgs(argc, `ma`):
return JS_EXCEPTION
)
jsBody.add(gen.jsCallAndRet)
let jsPragmas = newNimNode(nnkPragma).add(ident("cdecl"))
return newProc(gen.newName, params, jsBody, pragmas = jsPragmas)
func getFuncName(fun: NimNode; jsname, staticName: string): string =
if jsname != "":
return jsname
if staticName != "":
let i = staticName.find('.')
if i != -1:
return staticName.substr(i + 1)
return $fun[0]
proc addThisName(gen: var JSFuncGenerator; hasThis: bool) =
if hasThis:
var t = gen.funcParams[gen.i].t
if t.kind in {nnkPtrTy, nnkRefTy}:
t = t[0]
gen.thisTypeNode = t
gen.thisType = $t
gen.newName = ident($gen.t & "_" & gen.thisType & "_" & gen.funcName)
else:
let rt = gen.returnType.get
if rt.kind in {nnkRefTy, nnkPtrTy}:
gen.thisTypeNode = rt[0]
gen.thisType = rt[0].strVal
else:
if rt.kind == nnkBracketExpr:
gen.thisTypeNode = rt[1]
gen.thisType = rt[1].strVal
else:
gen.thisTypeNode = rt
gen.thisType = rt.strVal
gen.newName = ident($gen.t & "_" & gen.funcName)
proc initGenerator(fun: NimNode; t: BoundFunctionType; hasThis: bool;
jsname = ""; flag = bffNone; staticName = ""): JSFuncGenerator =
result = JSFuncGenerator(
t: t,
funcName: getFuncName(fun, jsname, staticName),
hasThis: hasThis,
dielabel: ident("ondie"),
jsFunCallList: newStmtList(),
jsFunCall: newCall(fun[0]),
flag: flag
)
result.readParams(fun)
if staticName == "":
result.addThisName(hasThis)
else:
result.thisType = staticName
if (let i = result.thisType.find('.'); i != -1):
result.thisType.setLen(i)
result.newName = ident($result.t & "_" & result.funcName)
proc makeJSCallAndRet(gen: var JSFuncGenerator; okstmt, errstmt: NimNode) =
let jfcl = gen.jsFunCallList
let dl = gen.dielabel
gen.jsCallAndRet = if gen.returnType.isSome:
quote do:
block `dl`:
return ctx.toJS(`jfcl`)
`errstmt`
else:
quote do:
block `dl`:
`jfcl`
`okstmt`
`errstmt`
macro jsctor*(fun: typed) =
var gen = initGenerator(fun, bfConstructor, hasThis = false)
gen.addRequiredParams()
gen.addOptionalParams()
gen.finishFunCallList()
let jfcl = gen.jsFunCallList
let dl = gen.dielabel
gen.jsCallAndRet = quote do:
block `dl`:
return ctx.toJSNew(`jfcl`, this)
return JS_EXCEPTION
let jsProc = gen.newJSProc(getJSParams())
gen.registerFunction()
return newStmtList(fun, jsProc)
macro jshasprop*(fun: typed) =
var gen = initGenerator(fun, bfPropertyHas, hasThis = true)
gen.addThisParam()
gen.addFixParam("atom")
gen.finishFunCallList()
let jfcl = gen.jsFunCallList
let dl = gen.dielabel
gen.jsCallAndRet = quote do:
block `dl`:
let retv = `jfcl`
return cint(retv)
return cint(-1)
let jsProc = gen.newJSProc(getJSHasPropParams(), false)
gen.registerFunction()
return newStmtList(fun, jsProc)
macro jsgetownprop*(fun: typed) =
var gen = initGenerator(fun, bfPropertyGetOwn, hasThis = true)
gen.addThisParam()
gen.addFixParam("prop")
var handleRetv: NimNode = nil
if gen.i < gen.funcParams.len:
handleRetv = quote do: discard
gen.jsFunCall.add(ident("desc"))
else:
handleRetv = quote do:
if desc != nil:
# From quickjs.h:
# > If 1 is returned, the property descriptor 'desc' is filled
# > if != NULL.
# So desc may be nil.
desc[].setter = JS_UNDEFINED
desc[].getter = JS_UNDEFINED
desc[].value = retv
desc[].flags = 0
gen.finishFunCallList()
let jfcl = gen.jsFunCallList
let dl = gen.dielabel
gen.jsCallAndRet = quote do:
block `dl`:
if JS_GetOpaque(this, JS_GetClassID(this)) == nil:
return cint(0)
let retv {.inject.} = ctx.toJS(`jfcl`)
if JS_IsException(retv):
return cint(-1)
if JS_IsUninitialized(retv):
return cint(0)
`handleRetv`
return cint(1)
return cint(-1)
let jsProc = gen.newJSProc(getJSGetOwnPropParams(), false)
gen.registerFunction()
return newStmtList(fun, jsProc)
macro jsgetprop*(fun: typed) =
var gen = initGenerator(fun, bfPropertyGet, hasThis = true)
gen.addThisParam("receiver")
gen.addFixParam("prop")
if gen.i < gen.funcParams.len:
gen.addFixParam("this")
gen.finishFunCallList()
let jfcl = gen.jsFunCallList
let dl = gen.dielabel
gen.jsCallAndRet = quote do:
block `dl`:
return ctx.toJS(`jfcl`)
return JS_EXCEPTION
let jsProc = gen.newJSProc(getJSGetPropParams(), false)
gen.registerFunction()
return newStmtList(fun, jsProc)
macro jssetprop*(fun: typed) =
var gen = initGenerator(fun, bfPropertySet, hasThis = true)
gen.addThisParam("receiver")
gen.addFixParam("atom")
gen.addFixParam("value")
if gen.i < gen.funcParams.len:
gen.addFixParam("this")
gen.finishFunCallList()
let jfcl = gen.jsFunCallList
let dl = gen.dielabel
gen.jsCallAndRet = if gen.returnType.isSome:
quote do:
block `dl`:
let v = toJS(ctx, `jfcl`)
if not JS_IsException(v):
return cint(1)
if JS_IsUninitialized(v):
return cint(0)
return cint(-1)
else:
quote do:
block `dl`:
`jfcl`
return cint(1)
return cint(-1)
let jsProc = gen.newJSProc(getJSSetPropParams(), false)
gen.registerFunction()
return newStmtList(fun, jsProc)
macro jsdelprop*(fun: typed) =
var gen = initGenerator(fun, bfPropertyDel, hasThis = true)
gen.addThisParam()
gen.addFixParam("prop")
gen.finishFunCallList()
let jfcl = gen.jsFunCallList
let dl = gen.dielabel
gen.jsCallAndRet = quote do:
block `dl`:
let retv = `jfcl`
return cint(retv)
return cint(-1)
let jsProc = gen.newJSProc(getJSDelPropParams(), false)
gen.registerFunction()
return newStmtList(fun, jsProc)
macro jspropnames*(fun: typed) =
var gen = initGenerator(fun, bfPropertyNames, hasThis = true)
gen.addThisParam()
gen.finishFunCallList()
let jfcl = gen.jsFunCallList
let dl = gen.dielabel
gen.jsCallAndRet = quote do:
block `dl`:
let retv = `jfcl`
ptab[] = retv.buffer
plen[] = retv.len
return cint(0)
return cint(-1)
let jsProc = gen.newJSProc(getJSPropNamesParams(), false)
gen.registerFunction()
return newStmtList(fun, jsProc)
macro jsfgetn(jsname: static string; flag: static BoundFunctionFlag;
fun: typed) =
var gen = initGenerator(fun, bfGetter, hasThis = true, jsname, flag)
if gen.actualMinArgs != 0 or gen.funcParams.len != gen.minArgs:
error("jsfget functions must only accept one parameter.")
if gen.returnType.isNone:
error("jsfget functions must have a return type.")
gen.addThisParam()
gen.finishFunCallList()
gen.makeJSCallAndRet(nil, quote do: discard)
let jsProc = if flag != bffReplaceable:
gen.newJSProc(getJSGetterParams(), false)
else:
gen.newJSProc(getJSMagicGetterParams(), false)
gen.registerFunction()
return newStmtList(fun, jsProc)
# "Why?" So the compiler doesn't cry.
# Warning: make these typed and you will cry instead.
template jsfget*(fun: untyped) =
jsfgetn("", bffNone, fun)
template jsuffget*(fun: untyped) =
jsfgetn("", bffUnforgeable, fun)
template jsrfget*(fun: untyped) =
jsfgetn("", bffReplaceable, fun)
template jsfget*(jsname, fun: untyped) =
jsfgetn(jsname, bffNone, fun)
template jsuffget*(jsname, fun: untyped) =
jsfgetn(jsname, bffUnforgeable, fun)
template jsrfget*(jsname, fun: untyped) =
jsfgetn(jsname, bffReplaceable, fun)
# Ideally we could simulate JS setters using nim setters, but nim setters
# won't accept types that don't match their reflected field's type.
macro jsfsetn(jsname: static string; fun: typed) =
var gen = initGenerator(fun, bfSetter, hasThis = true, jsname = jsname)
if gen.actualMinArgs != 1 or gen.funcParams.len != gen.minArgs:
error("jsfset functions must accept two parameters")
#TODO should check if result is JSResult[void]
gen.addThisParam()
gen.addFixParam("val")
gen.finishFunCallList()
# return param anyway
let okstmt = quote do: discard
let errstmt = quote do: return JS_DupValue(ctx, val)
gen.makeJSCallAndRet(okstmt, errstmt)
let jsProc = gen.newJSProc(getJSSetterParams(), false)
gen.registerFunction()
return newStmtList(fun, jsProc)
template jsfset*(fun: untyped) =
jsfsetn("", fun)
template jsfset*(jsname, fun: untyped) =
jsfsetn(jsname, fun)
macro jsfuncn*(jsname: static string; flag: static BoundFunctionFlag;
staticName: static string; fun: typed) =
var gen = initGenerator(fun, bfFunction, hasThis = true, jsname = jsname,
flag = flag, staticName = staticName)
if gen.minArgs == 0 and gen.flag != bffStatic:
#TODO this should work, especially with JSContext.
error("Zero-parameter functions are not supported. " &
"(Maybe pass Window or Client?)")
if gen.flag != bffStatic:
gen.addThisParam()
gen.addRequiredParams()
gen.addOptionalParams()
gen.finishFunCallList()
let okstmt = quote do:
return JS_UNDEFINED
let errstmt = quote do:
return JS_EXCEPTION
gen.makeJSCallAndRet(okstmt, errstmt)
let jsProc = gen.newJSProc(getJSParams())
gen.registerFunction()
return newStmtList(fun, jsProc)
template jsfunc*(fun: untyped) =
jsfuncn("", bffNone, "", fun)
template jsuffunc*(fun: untyped) =
jsfuncn("", bffUnforgeable, "", fun)
template jsfunc*(jsname, fun: untyped) =
jsfuncn(jsname, bffNone, "", fun)
template jsuffunc*(jsname, fun: untyped) =
jsfuncn(jsname, bffUnforgeable, "", fun)
template jsstfunc*(name, fun: untyped) =
jsfuncn("", bffStatic, name, fun)
macro jsfin*(fun: typed) =
var gen = initGenerator(fun, bfFinalizer, hasThis = true)
let finName = gen.newName
let finFun = ident(gen.funcName)
let t = gen.thisTypeNode
var finStmt: NimNode = nil # warning: won't compile on 2.0.4 with let
if gen.minArgs == 1:
finStmt = quote do: `finFun`(cast[`t`](opaque))
elif gen.minArgs == 2:
finStmt = quote do: `finFun`(rt, cast[`t`](opaque))
else:
error("Expected one or two parameters")
let jsProc = quote do:
proc `finName`(rt {.inject.}: JSRuntime; opaque {.inject.}: pointer) =
`finStmt`
gen.registerFunction()
return newStmtList(fun, jsProc)
# Having the same names for these and the macros leads to weird bugs, so the
# macros get an additional f.
template jsget*() {.pragma.}
template jsget*(name: string) {.pragma.}
template jsset*() {.pragma.}
template jsset*(name: string) {.pragma.}
template jsgetset*() {.pragma.}
template jsgetset*(name: string) {.pragma.}
template jsufget*() {.pragma.}
template jsufget*(name: string) {.pragma.}
template jsrget*() {.pragma.}
template jsrget*(name: string) {.pragma.}
proc js_illegal_ctor*(ctx: JSContext; this: JSValueConst; argc: cint;
argv: JSValueConstArray): JSValue {.cdecl.} =
return JS_ThrowTypeError(ctx, "Illegal constructor")
type
JSObjectPragma = object
name: string
varsym: NimNode
flag: BoundFunctionFlag
func getPragmaName(varPragma: NimNode): string =
if varPragma.kind == nnkExprColonExpr:
return $varPragma[0]
return $varPragma
func getStringFromPragma(varPragma: NimNode): Option[string] =
if varPragma.kind == nnkExprColonExpr:
if not varPragma.len == 1 and varPragma[1].kind == nnkStrLit:
error("Expected string as pragma argument")
return some($varPragma[1])
return none(string)
func tname(info: RegistryInfo): string =
return info.t.strVal
# Differs from tname if the Nim object's name differs from the JS object's
# name.
func jsname(info: RegistryInfo): string =
if info.name != "":
return info.name
return info.tname
proc registerGetter(stmts: NimNode; info: RegistryInfo; op: JSObjectPragma) =
let t = info.t
let tname = info.tname
let node = op.varsym
let fn = op.name
let id = ident($bfGetter & "_" & tname & "_" & fn)
stmts.add(quote do:
proc `id`(ctx: JSContext; this: JSValueConst): JSValue {.cdecl.} =
when `t` is object:
var arg_0 {.noinit.}: ptr `t`
else:
var arg_0: `t`
if ctx.fromJSThis(this, arg_0).isNone:
return JS_EXCEPTION
when arg0.`node` is JSValue:
return JS_DupValue(ctx, arg0.`node`)
elif arg_0.`node` is object:
return ctx.toJSP(arg_0, arg_0.`node`)
else:
return ctx.toJS(arg_0.`node`)
)
info.registerFunction(BoundFunction(
t: bfGetter,
name: fn,
id: id,
flag: op.flag
))
proc registerSetter(stmts: NimNode; info: RegistryInfo; op: JSObjectPragma) =
let t = info.t
let tname = info.tname
let node = op.varsym
let fn = op.name
let id = ident($bfSetter & "_" & tname & "_" & fn)
stmts.add(quote do:
proc `id`(ctx: JSContext; this, val: JSValueConst): JSValue {.cdecl.} =
when `t` is object:
var arg_0 {.noinit.}: ptr `t`
else:
var arg_0: `t`
if ctx.fromJSThis(this, arg_0).isNone:
return JS_EXCEPTION
# We can't just set arg_0.`node` directly, or fromJS may damage it.
var nodeVal: typeof(arg_0.`node`)
when nodeVal is JSValue:
static:
error(".jsset is not supported on JSValue; use jsfset")
else:
if ctx.fromJS(val, nodeVal).isNone:
return JS_EXCEPTION
arg_0.`node` = move(nodeVal)
return JS_DupValue(ctx, val)
)
info.registerFunction(BoundFunction(t: bfSetter, name: fn, id: id))
proc registerPragmas(stmts: NimNode; info: RegistryInfo; t: NimNode) =
let typ = t.getTypeInst()[1] # The type, as declared.
var impl = typ.getTypeImpl() # ref t
if impl.kind in {nnkRefTy, nnkPtrTy}:
impl = impl[0].getImpl()
else:
impl = typ.getImpl()
# stolen from std's macros.customPragmaNode
var identDefsStack = newSeq[NimNode](impl[2].len)
for i, it in identDefsStack.mpairs:
it = impl[2][i]
while identDefsStack.len > 0:
let identDefs = identDefsStack.pop()
case identDefs.kind
of nnkRecList:
for child in identDefs.children:
identDefsStack.add(child)
of nnkRecCase:
discard # case objects are not supported
else:
for i in 0 ..< identDefs.len - 2:
var varNode = identDefs[i]
if varNode.kind == nnkPragmaExpr:
let varPragmas = varNode[1]
varNode = varNode[0]
if varNode.kind == nnkPostfix:
varNode = varNode[1]
for varPragma in varPragmas:
let pragmaName = getPragmaName(varPragma)
var op = JSObjectPragma(
name: getStringFromPragma(varPragma).get($varNode),
varsym: varNode
)
case pragmaName
of "jsget": stmts.registerGetter(info, op)
of "jsset": stmts.registerSetter(info, op)
of "jsufget": # LegacyUnforgeable
op.flag = bffUnforgeable
stmts.registerGetter(info, op)
of "jsrget": # Replaceable
op.flag = bffReplaceable
stmts.registerGetter(info, op)
of "jsgetset":
stmts.registerGetter(info, op)
stmts.registerSetter(info, op)
elif varNode.kind == nnkPostfix:
varNode = varNode[1]
let typ = identDefs[^2]
if typ.getTypeInst().sameType(JSValue.getType()) or
JSValue.getType().sameType(typ):
info.markList.add(quote do:
JS_MarkValue(rt, this.`varNode`, markFunc)
)
proc nim_finalize_for_js*(obj, typeptr: pointer) =
var fin: JSFinalizerFunction = nil
for rt in runtimes:
let rtOpaque = rt.getOpaque()
fin = rtOpaque.fins.getOrDefault(typeptr)
rtOpaque.plist.withValue(obj, v):
let p = v[].p
let val = JS_MKPTR(JS_TAG_OBJECT, p)
if fin != nil:
fin(rt, obj)
JS_SetOpaque(val, nil)
rtOpaque.plist.del(obj)
if rtOpaque.destroying == obj:
# Allow QJS to collect the JSValue through checkDestroy.
rtOpaque.destroying = nil
else:
JS_FreeValueRT(rt, val)
return
# No JSValue exists for the object, but it likely still expects us to
# free it.
# We pass nil as the runtime, since that's the only sensible solution.
if fin != nil:
fin(nil, obj)
type
TabGetSet* = object
name*: string
get*: JSGetterMagicFunction
set*: JSSetterMagicFunction
magic*: int16
TabFunc* = object
name*: string
fun*: JSCFunction
template jsDestructor*[U](T: typedesc[ref U]) =
static:
jsDtors.incl($T)
proc `=destroy`(obj: var U) =
nim_finalize_for_js(addr obj, getTypePtr(T))
template jsDestructor*(T: typedesc[object]) =
static:
jsDtors.incl($T)
proc `=destroy`(obj: var T) =
nim_finalize_for_js(addr obj, getTypePtr(T))
proc bindConstructor(stmts: NimNode; info: var RegistryInfo): NimNode =
if info.ctorFun != nil:
return info.ctorFun
return ident("js_illegal_ctor")
proc bindReplaceableSet(stmts: NimNode; info: var RegistryInfo) =
let rsf = ident("js_replaceable_set")
let t = info.t
info.replaceableSetFun = rsf
let trns = info.tabReplaceableNames
stmts.add(quote do:
const replaceableNames = `trns`
proc `rsf`(ctx: JSContext; this, val: JSValueConst; magic: cint): JSValue
{.cdecl.} =
when `t` is object:
var dummy {.noinit.}: ptr `t`
else:
var dummy: `t`
if ctx.fromJSThis(this, dummy).isNone:
return JS_EXCEPTION
let name = replaceableNames[int(magic)]
let dval = JS_DupValue(ctx, val)
if JS_DefinePropertyValueStr(ctx, this, name, dval, JS_PROP_C_W_E) < 0:
return JS_EXCEPTION
return JS_DupValue(ctx, val)
)
proc bindGetSet(stmts: NimNode; info: RegistryInfo) =
var replaceableId = 0u16
for k, (get, set, flag) in info.getset:
case flag
of bffNone:
info.tabList.add(quote do: JS_CGETSET_DEF(`k`, `get`, `set`))
of bffUnforgeable:
info.tabUnforgeable.add(quote do:
JS_CGETSET_DEF_NOCONF(`k`, `get`, `set`))
of bffReplaceable:
if set != nil:
error("Replaceable properties must not have a setter.")
let orid = replaceableId
inc replaceableId
if orid > replaceableId:
error("Too many replaceable functions defined.")
let magic = cast[int16](orid)
info.tabList.add(quote do:
JS_CGETSET_MAGIC_DEF(`k`, `get`, js_replaceable_set, `magic`))
else:
error("Static getters and setters are not supported.")
proc bindExtraGetSet(stmts: NimNode; info: var RegistryInfo;
extraGetSet: openArray[TabGetSet]) =
for x in extraGetSet:
let k = x.name
let g = x.get
let s = x.set
let m = x.magic
info.tabList.add(quote do: JS_CGETSET_MAGIC_DEF(`k`, `g`, `s`, `m`))
proc jsCheckDestroyRef*(rt: JSRuntime; val: JSValueConst): JS_BOOL {.cdecl.} =
let opaque = JS_GetOpaque(val, JS_GetClassID(val))
if opaque != nil:
# Before this function is called, the ownership model is
# JSObject -> Nim object.
# Here we change it to Nim object -> JSObject.
# As a result, Nim object's reference count can now reach zero (it is
# no longer "referenced" by the JS object).
# nim_finalize_for_js will be invoked by the Nim GC when the Nim
# refcount reaches zero. Then, the JS object's opaque will be set
# to nil, and its refcount decreased again, so next time this
# function will return true.
#
# Actually, we need another hack to ensure correct
# operation. GC_unref may call the destructor of this object, and
# in this case we cannot ask QJS to keep the JSValue alive. So we set
# the "destroying" pointer to the current opaque, and return true if
# the opaque was collected.
rt.getOpaque().destroying = opaque
# We can lie about the type in refc, as it type erases the reference.
# Sadly, this won't work in ARC... then again, nothing works in ARC,
# so whatever.
GC_unref(cast[RootRef](opaque))
if rt.getOpaque().destroying == nil:
# Looks like GC_unref called nim_finalize_for_js for this pointer.
# This means we can allow QJS to collect this JSValue.
return true
else:
let rtOpaque = rt.getOpaque()
rtOpaque.destroying = nil
# Set an internal flag to note that the JS object is owned by the
# Nim side.
# This means that if toJS is used again on the Nim object, JS will
# first get a non-dup'd object, and a reference will be set on
# the Nim object.
#
#TODO can we eliminate this hash somehow?
# at least I *think* in the common case of "no reference cycle",
# we could just elide the jsref set here, and add a reference
# in toJSP0 if the refcount on the JS pointer is 0. (however,
# we must do it here if the refcount is > 1, that means we have
# a cycle.)
# it sounds too hacky to try for now, but may be worth it if this
# turns out to be a bottleneck...
rtOpaque.plist.withValue(opaque, v):
v[].jsref = false
# Returning false from this function signals to the QJS GC that it
# should not be collected yet. Accordingly, the JSObject's refcount
# will be set to one again.
return false
return true
proc jsCheckDestroyNonRef*(rt: JSRuntime; val: JSValueConst): JS_BOOL
{.cdecl.} =
let opaque = JS_GetOpaque(val, JS_GetClassID(val))
if opaque != nil:
# This is not a reference, just a pointer with a reference to the
# root ancestor object.
# Remove the reference, allowing destruction of the root object once
# again.
let rtOpaque = rt.getOpaque()
var parent: pointer = nil
discard rtOpaque.parentMap.pop(opaque, parent)
GC_unref(cast[RootRef](parent))
# Of course, nim_finalize_for_js might only be called later for
# this object, because the parent can still have references to it.
# (And for the same reason, a reference to the same object might
# still be necessary.)
# Accordingly, we return false here as well.
return false
return true
proc bindEndStmts(endstmts: NimNode; info: RegistryInfo) =
let jsname = info.jsname
let dfin = info.dfin
let markFun = info.markFun
if info.propGetOwnFun.kind != nnkNilLit or
info.propGetFun.kind != nnkNilLit or
info.propSetFun.kind != nnkNilLit or
info.propDelFun.kind != nnkNilLit or
info.propHasFun.kind != nnkNilLit or
info.propNamesFun.kind != nnkNilLit:
let propGetOwnFun = info.propGetOwnFun
let propGetFun = info.propGetFun
let propSetFun = info.propSetFun
let propDelFun = info.propDelFun
let propHasFun = info.propHasFun
let propNamesFun = info.propNamesFun
endstmts.add(quote do:
var exotic {.global.} = JSClassExoticMethods(
get_own_property: `propGetOwnFun`,
get_own_property_names: `propNamesFun`,
has_property: `propHasFun`,
get_property: `propGetFun`,
set_property: `propSetFun`,
delete_property: `propDelFun`
)
var cd {.global.} = JSClassDef(
class_name: `jsname`,
can_destroy: `dfin`,
gc_mark: `markFun`,
exotic: JSClassExoticMethodsConst(addr exotic)
)
let classDef {.inject.} = JSClassDefConst(addr cd)
)
else:
endstmts.add(quote do:
var cd {.global.} = JSClassDef(
class_name: `jsname`,
can_destroy: `dfin`,
gc_mark: `markFun`
)
let classDef {.inject.} = JSClassDefConst(addr cd)
)
proc bindMarkFunc(stmts: NimNode; info: RegistryInfo) =
let t = info.t
let id = ident("mark_" & info.tname)
let markList = info.markList
stmts.add(quote do:
proc `id`(rt {.inject.}: JSRuntime; val: JSValueConst;
markFunc {.inject.}: JS_MarkFunc) {.cdecl.} =
let p = JS_GetOpaque(val, JS_GetClassID(val))
# Disgusting cast, but try not to confuse refc.
let this {.inject.} = cast[ptr typeof(`t`.default[])](p)
`markList`
)
info.markFun = id
macro registerType*(ctx: JSContext; t: typed; parent: JSClassID = 0;
asglobal: static bool = false; globalparent: static bool = false;
nointerface = false; name: static string = "";
hasExtraGetSet: static bool = false;
extraGetSet: static openArray[TabGetSet] = []; namespace = JS_NULL):
JSClassID =
var stmts = newStmtList()
var info = BoundFunctions.getOrDefault(t.strVal)
if info == nil:
info = newRegistryInfo(t)
if name != "":
info.name = name
if not asglobal:
let impl = t.getTypeInst()[1].getTypeImpl()
if impl.kind == nnkRefTy:
info.dfin = quote do: jsCheckDestroyRef
else:
info.dfin = quote do: jsCheckDestroyNonRef
if info.tname notin jsDtors:
warning("No destructor has been defined for type " & info.tname)
else:
info.dfin = newNilLit()
if info.tname in jsDtors:
error("Global object " & info.tname & " must not have a destructor!")
stmts.registerPragmas(info, t)
if info.markList.len > 0:
stmts.bindMarkFunc(info)
if info.tabReplaceableNames.len > 0:
stmts.bindReplaceableSet(info)
stmts.bindGetSet(info)
if hasExtraGetSet:
#HACK: for some reason, extraGetSet gets weird contents when nothing is
# passed to it. So we need an extra flag to signal if anything has
# been passed to it at all.
stmts.bindExtraGetSet(info, extraGetSet)
let sctr = stmts.bindConstructor(info)
let endstmts = newStmtList()
endstmts.bindEndStmts(info)
let tabList = info.tabList
let finName = info.finName
let unforgeable = info.tabUnforgeable
let staticfuns = info.tabStatic
let global = asglobal and not globalparent
endstmts.add(quote do:
`ctx`.newJSClass(classDef, getTypePtr(`t`), `sctr`, `tabList`, `parent`,
`global`, `nointerface`, `finName`, `namespace`, `unforgeable`,
`staticfuns`)
)
stmts.add(newBlockStmt(endstmts))
return stmts
proc addRow(s: var string; title: string; count, size, sz2, cnt2: int64;
name: string) =
var fv = $(float(sz2) / float(cnt2))
let i = fv.find('.')
if i != -1:
fv.setLen(i + 1)
else:
fv &= ".0"
s &= title & ": " & $count & " " & $size & " (" & fv & ")" & name & "\n"
proc addRow(s: var string; title: string; count, size, sz2: int64;
name: string) =
s.addRow(title, count, size, sz2, count, name)
proc addRow(s: var string; title: string; count, size: int64; name: string) =
s.addRow(title, count, size, size, name)
proc getMemoryUsage*(rt: JSRuntime): string =
var m: JSMemoryUsage
JS_ComputeMemoryUsage(rt, m)
var s = ""
if m.malloc_count != 0:
s.addRow("memory allocated", m.malloc_count, m.malloc_size, "/block")
s.addRow("memory used", m.memory_used_count, m.memory_used_size,
m.malloc_size - m.memory_used_size, " average slack")
if m.atom_count != 0:
s.addRow("atoms", m.atom_count, m.atom_size, "/atom")
if m.str_count != 0:
s.addRow("strings", m.str_count, m.str_size, "/string")
if m.obj_count != 0:
s.addRow("objects", m.obj_count, m.obj_size, "/object")
s.addRow("properties", m.prop_count, m.prop_size, m.prop_size, m.obj_count,
"/object")
s.addRow("shapes", m.shape_count, m.shape_size, "/shape")
if m.js_func_count != 0:
s.addRow("js functions", m.js_func_count, m.js_func_size, "/function")
if m.c_func_count != 0:
s &= "native functions: " & $m.c_func_count & "\n"
if m.array_count != 0:
s &= "arrays: " & $m.array_count & "\n" &
"fast arrays: " & $m.fast_array_count & "\n"
s.addRow("fast array elements", m.fast_array_elements,
m.fast_array_elements * sizeof(JSValue), m.fast_array_elements,
m.fast_array_count, "")
if m.binary_object_count != 0:
s &= "binary objects: " & $m.binary_object_count & " " &
$m.binary_object_size
move(s)
proc eval*(ctx: JSContext; s: string; file = "";
evalFlags = JS_EVAL_TYPE_GLOBAL): JSValue =
return JS_Eval(ctx, cstring(s), csize_t(s.len), cstring(file),
cint(evalFlags))
proc compileScript*(ctx: JSContext; s: string; file = ""): JSValue =
return ctx.eval(s, file, JS_EVAL_FLAG_COMPILE_ONLY)
proc compileModule*(ctx: JSContext; s: string; file = ""): JSValue =
return ctx.eval(s, file, JS_EVAL_TYPE_MODULE or JS_EVAL_FLAG_COMPILE_ONLY)
proc evalFunction*(ctx: JSContext; val: JSValue): JSValue =
return JS_EvalFunction(ctx, val)
{.pop.} # raises