diff options
Diffstat (limited to 'lib/monoucha0/monoucha/javascript.nim')
-rw-r--r-- | lib/monoucha0/monoucha/javascript.nim | 1590 |
1 files changed, 1590 insertions, 0 deletions
diff --git a/lib/monoucha0/monoucha/javascript.nim b/lib/monoucha0/monoucha/javascript.nim new file mode 100644 index 00000000..856f15d8 --- /dev/null +++ b/lib/monoucha0/monoucha/javascript.nim @@ -0,0 +1,1590 @@ +## 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.) +## {.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, 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] + + BoundFunction = object + t: BoundFunctionType + name: string + id: NimNode + magic: uint16 + unforgeable: bool + isstatic: bool + ctorBody: NimNode + + 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" + +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 p in rtOpaque.plist.values: + rtOpaque.tmplist[np] = 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] + #TODO maybe finalize? + let val = JS_MKPTR(JS_TAG_OBJECT, p) + let classid = JS_GetClassID(val) + rtOpaque.fins.withValue(classid, fin): + fin[](rt, val) + 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 + +func getClass*(ctx: JSContext; class: cstring): JSClassID = + ## Get the class ID of the registered class `class'. + ## Note: this uses the Nim type's name, **not** the JS type's name. + try: + return ctx.getOpaque().creg[class] + except KeyError: + raise newException(Defect, "Class does not exist") + +func hasClass*(ctx: JSContext; class: cstring): bool = + ## Check if `class' is registered. + ## Note: this uses the Nim type's name, **not** the JS type's name. + return class in ctx.getOpaque().creg + +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 v in opaque.errCtorRefs: + 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: JSValue; + 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) + +func newJSClass*(ctx: JSContext; cdef: JSClassDefConst; tname: cstring; + nimt: pointer; ctor: JSCFunction; funcs: JSFunctionList; parent: JSClassID; + asglobal: bool; nointerface: bool; finalizer: JSFinalizerFunction; + namespace: JSValue; errid: Opt[JSErrorEnum]; + unforgeable, staticfuns: JSFunctionList; ishtmldda: bool): JSClassID + {.discardable.} = + result = 0 + let rt = JS_GetRuntime(ctx) + discard JS_NewClassID(rt, result) + var ctxOpaque = ctx.getOpaque() + var 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 + ctxOpaque.creg[tname] = result + if ctxOpaque.parents.len <= int(result): + ctxOpaque.parents.setLen(int(result) + 1) + ctxOpaque.parents[result] = parent + if ishtmldda: + ctxOpaque.htmldda = result + if finalizer != nil: + rtOpaque.fins[result] = 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] + ctx.defineProperty(proto, itSym, val) + let news = JS_NewAtomString(ctx, cdef.class_name) + doAssert not JS_IsException(news) + ctx.definePropertyC(proto, ctxOpaque.symRefs[jsyToStringTag], + JS_DupValue(ctx, news)) + JS_SetClassProto(ctx, result, proto) + ctx.addClassUnforgeable(proto, result, parent, unforgeable) + if asglobal: + let global = ctxOpaque.global + assert ctxOpaque.gclass == 0 + ctxOpaque.gclass = result + ctx.definePropertyC(global, ctxOpaque.symRefs[jsyToStringTag], + JS_DupValue(ctx, news)) + 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 = JS_NewCFunction2(ctx, ctor, cstring($cdef.class_name), 0, + JS_CFUNC_constructor, 0) + 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) + if errid.isSome: + ctx.getOpaque().errCtorRefs[errid.get] = JS_DupValue(ctx, jctor) + while ctxOpaque.ctors.len <= int(result): + ctxOpaque.ctors.add(JS_UNDEFINED) + ctxOpaque.ctors[result] = JS_DupValue(ctx, jctor) + if not nointerface: + if JS_IsNull(namespace): + ctx.definePropertyCW(ctxOpaque.global, $cdef.class_name, jctor) + else: + ctx.definePropertyCW(namespace, $cdef.class_name, jctor) + else: + JS_FreeValue(ctx, jctor) + +type FuncParam = tuple + name: string + t: NimNode + val: Option[NimNode] + generic: Option[NimNode] + isptr: bool + +func getMinArgs(params: seq[FuncParam]): int = + for i, it in params: + if it[2].isSome: + return i + let t = it.t + if t.kind == nnkBracketExpr: + if t.typeKind == varargs.getType().typeKind: + assert i == params.high, "Not even nim can properly handle this..." + return i + return params.len + +type + JSFuncGenerator = ref object + t: BoundFunctionType + hasThis: bool + funcName: string + funcParams: seq[FuncParam] + passCtx: bool + thisType: string + thisTypeNode: NimNode + returnType: Option[NimNode] + newName: NimNode + newBranchList: seq[NimNode] + errval: NimNode # JS_EXCEPTION or -1 + # die: didn't match parameters, but could still match other ones + dielabel: NimNode + jsFunCallLists: seq[NimNode] + jsFunCallList: NimNode + jsFunCall: NimNode + jsCallAndRet: NimNode + minArgs: int + actualMinArgs: int # minArgs without JSContext + i: int # nim parameters accounted for + j: int # js parameters accounted for (not including fix ones, e.g. `this') + unforgeable: bool + isstatic: bool + +var BoundFunctions {.compileTime.}: Table[string, seq[BoundFunction]] + +proc getParams(fun: NimNode): seq[FuncParam] = + let formalParams = fun.findChild(it.kind == nnkFormalParams) + var funcParams: seq[FuncParam] = @[] + var returnType = none(NimNode) + if formalParams[0].kind != nnkEmpty: + returnType = some(formalParams[0]) + for i in 1 ..< fun.params.len: + let it = formalParams[i] + let tt = it[^2] + var t: NimNode + if it[^2].kind != nnkEmpty: + t = `tt` + elif it[^1].kind != nnkEmpty: + let x = it[^1] + t = quote do: + typeof(`x`) + else: + error("?? " & treeRepr(it)) + let isptr = t.kind == nnkVarTy + if t.kind == nnkRefTy: + t = t[0] + elif t.kind == nnkVarTy: + t = newNimNode(nnkPtrTy).add(t[0]) + let val = if it[^1].kind != nnkEmpty: + let x = it[^1] + some(newPar(x)) + else: + none(NimNode) + var g = none(NimNode) + for i in 0 ..< it.len - 2: + let name = $it[i] + funcParams.add((name, t, val, g, isptr)) + funcParams + +proc getReturn(fun: NimNode): Option[NimNode] = + let formalParams = fun.findChild(it.kind == nnkFormalParams) + if formalParams[0].kind != nnkEmpty: + some(formalParams[0]) + else: + none(NimNode) + +template getJSParams(): untyped = + [ + (quote do: JSValue), + newIdentDefs(ident("ctx"), quote do: JSContext), + newIdentDefs(ident("this"), quote do: JSValue), + newIdentDefs(ident("argc"), quote do: cint), + newIdentDefs(ident("argv"), quote do: ptr UncheckedArray[JSValue]) + ] + +template getJSGetterParams(): untyped = + [ + (quote do: JSValue), + newIdentDefs(ident("ctx"), quote do: JSContext), + newIdentDefs(ident("this"), quote do: JSValue), + ] + +template getJSGetOwnPropParams(): untyped = + [ + (quote do: cint), + newIdentDefs(ident("ctx"), quote do: JSContext), + newIdentDefs(ident("desc"), quote do: ptr JSPropertyDescriptor), + newIdentDefs(ident("this"), quote do: JSValue), + newIdentDefs(ident("prop"), quote do: JSAtom), + ] + +template getJSGetPropParams(): untyped = + [ + (quote do: JSValue), + newIdentDefs(ident("ctx"), quote do: JSContext), + newIdentDefs(ident("this"), quote do: JSValue), + newIdentDefs(ident("prop"), quote do: JSAtom), + newIdentDefs(ident("receiver"), quote do: JSValue), + ] + +template getJSSetPropParams(): untyped = + [ + (quote do: cint), + newIdentDefs(ident("ctx"), quote do: JSContext), + newIdentDefs(ident("this"), quote do: JSValue), + newIdentDefs(ident("atom"), quote do: JSAtom), + newIdentDefs(ident("value"), quote do: JSValue), + newIdentDefs(ident("receiver"), quote do: JSValue), + newIdentDefs(ident("flags"), quote do: cint), + ] + +template getJSDelPropParams(): untyped = + [ + (quote do: cint), + newIdentDefs(ident("ctx"), quote do: JSContext), + newIdentDefs(ident("this"), quote do: JSValue), + newIdentDefs(ident("prop"), quote do: JSAtom), + ] + +template getJSHasPropParams(): untyped = + [ + (quote do: cint), + newIdentDefs(ident("ctx"), quote do: JSContext), + newIdentDefs(ident("this"), quote do: JSValue), + newIdentDefs(ident("atom"), quote do: JSAtom), + ] + + +template getJSSetterParams(): untyped = + [ + (quote do: JSValue), + newIdentDefs(ident("ctx"), quote do: JSContext), + newIdentDefs(ident("this"), quote do: JSValue), + newIdentDefs(ident("val"), quote do: JSValue), + ] + +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: JSValue) + ] + +template fromJS_or_die*(ctx, val, res, dl: untyped) = + if ctx.fromJS(val, res).isNone: + break dl + +proc addParam2(gen: var JSFuncGenerator; s, t, val: NimNode; + fallback: NimNode = nil) = + let dl = gen.dielabel + if fallback == nil: + for list in gen.jsFunCallLists.mitems: + list.add(quote do: + var `s`: `t` + fromJS_or_die(ctx, `val`, `s`, `dl`) + ) + else: + let j = gen.j + for list in gen.jsFunCallLists.mitems: + list.add(quote do: + var `s`: `t` + if `j` < argc and not JS_IsUndefined(argv[`j`]): + fromJS_or_die(ctx, `val`, `s`, `dl`) + else: + `s` = `fallback` + ) + +proc addValueParam(gen: var JSFuncGenerator; s, t: NimNode; + fallback: NimNode = nil) = + let j = gen.j + gen.addParam2(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 tt = gen.thisType + let fn = gen.funcName + let ev = gen.errval + for list in gen.jsFunCallLists.mitems: + list.add(quote do: + var `s`: `t` + if ctx.fromJSThis(`id`, `s`).isNone: + discard JS_ThrowTypeError(ctx, + "'%s' called on an object that is not an instance of %s", `fn`, `tt`) + return `ev` + ) + if gen.funcParams[gen.i].isptr: + 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) + if t.typeKind == ntyGenericParam: + error("Union parameters are no longer supported. Use JSValue instead.") + gen.addParam2(s, t, id) + if gen.funcParams[gen.i].isptr: + 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 + if tt.typeKind == ntyGenericParam: + error("Union parameters are no longer supported. Use JSValue instead.") + gen.addValueParam(s, tt) + if gen.funcParams[gen.i].isptr: + 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(JSValue.getType()) or JSValue.getType().sameType(vt): + s = quote do: + argv.toOpenArray(`j`, argc - 1) + else: + error("Only JSValue varargs are supported") + else: + if gen.funcParams[gen.i][2].isNone: + error("No fallback value. Maybe a non-optional parameter follows an " & + "optional parameter?") + let fallback = gen.funcParams[gen.i][2].get + if tt.typeKind == ntyGenericParam: + error("Union parameters are no longer supported. Use JSValue instead.") + gen.addValueParam(s, tt, fallback) + if gen.funcParams[gen.i].isptr: + s = quote do: `s`[] + gen.jsFunCall.add(s) + inc gen.j + inc gen.i + +proc finishFunCallList(gen: var JSFuncGenerator) = + for branch in gen.jsFunCallLists: + branch.add(gen.jsFunCall) + +var jsDtors {.compileTime.}: HashSet[string] + +proc registerFunction(typ: string; nf: BoundFunction) = + BoundFunctions.withValue(typ, val): + val[].add(nf) + do: + BoundFunctions[typ] = @[nf] + +proc registerFunction(typ: string; t: BoundFunctionType; name: string; + id: NimNode; magic: uint16 = 0; uf = false; isstatic = false; + ctorBody: NimNode = nil) = + registerFunction(typ, BoundFunction( + t: t, + name: name, + id: id, + magic: magic, + unforgeable: uf, + isstatic: isstatic, + ctorBody: ctorBody + )) + +proc registerConstructor(gen: JSFuncGenerator; jsProc: NimNode) = + registerFunction(gen.thisType, gen.t, gen.funcName, gen.newName, + uf = gen.unforgeable, isstatic = gen.isstatic, ctorBody = jsProc) + +proc registerFunction(gen: JSFuncGenerator) = + registerFunction(gen.thisType, gen.t, gen.funcName, gen.newName, + uf = gen.unforgeable, isstatic = gen.isstatic) + +proc newJSProcBody(gen: var JSFuncGenerator; isva: bool): NimNode = + let ma = gen.actualMinArgs + result = newStmtList() + if isva and ma > 0: + result.add(quote do: + if argc < `ma`: + return JS_ThrowTypeError(ctx, + "At least %d arguments required, but only %d passed", cint(`ma`), + cint(argc)) + ) + result.add(gen.jsCallAndRet) + +proc newJSProc(gen: var JSFuncGenerator; params: openArray[NimNode]; + isva = true): NimNode = + let jsBody = gen.newJSProcBody(isva) + 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] + +func getErrVal(t: BoundFunctionType): NimNode = + if t in {bfPropertyGetOwn, bfPropertySet, bfPropertyDel, bfPropertyHas, + bfPropertyNames}: + return quote do: cint(-1) + return quote do: JS_EXCEPTION + +proc addJSContext(gen: var JSFuncGenerator) = + if gen.funcParams.len > gen.i: + if gen.funcParams[gen.i].t.eqIdent(ident("JSContext")): + gen.passCtx = true + gen.jsFunCall.add(ident("ctx")) + inc gen.i + elif gen.funcParams[gen.i].t.eqIdent(ident("JSRuntime")): + inc gen.i # special case for finalizers that have a JSRuntime param + +proc addThisName(gen: var JSFuncGenerator; hasThis: bool) = + if hasThis: + var t = gen.funcParams[gen.i].t + if t.kind == nnkPtrTy: + 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) + +func getActualMinArgs(gen: var JSFuncGenerator): int = + var ma = gen.minArgs + if gen.hasThis and not gen.isstatic: + dec ma + if gen.passCtx: + dec ma + assert ma >= 0 + return ma + +proc initGenerator(fun: NimNode; t: BoundFunctionType; hasThis: bool; + jsname = ""; unforgeable = false; staticName = ""): JSFuncGenerator = + let jsFunCallList = newStmtList() + let funcParams = getParams(fun) + var gen = JSFuncGenerator( + t: t, + funcName: getFuncName(fun, jsname, staticName), + funcParams: funcParams, + returnType: getReturn(fun), + minArgs: funcParams.getMinArgs(), + hasThis: hasThis, + errval: getErrVal(t), + dielabel: ident("ondie"), + jsFunCallList: jsFunCallList, + jsFunCallLists: @[jsFunCallList], + jsFunCall: newCall(fun[0]), + unforgeable: unforgeable, + isstatic: staticName != "" + ) + gen.addJSContext() + gen.actualMinArgs = gen.getActualMinArgs() # must come after passctx is set + if staticName == "": + gen.addThisName(hasThis) + else: + gen.thisType = staticName + if (let i = gen.thisType.find('.'); i != -1): + gen.thisType.setLen(i) + gen.newName = ident($gen.t & "_" & gen.funcName) + return gen + +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` + +proc makeCtorJSCallAndRet(gen: var JSFuncGenerator; errstmt: NimNode) = + let jfcl = gen.jsFunCallList + let dl = gen.dielabel + gen.jsCallAndRet = quote do: + block `dl`: + return ctx.toJSNew(`jfcl`, this) + `errstmt` + +macro jsctor*(fun: typed) = + var gen = initGenerator(fun, bfConstructor, hasThis = false) + gen.addRequiredParams() + gen.addOptionalParams() + gen.finishFunCallList() + let errstmt = quote do: + return JS_EXCEPTION + gen.makeCtorJSCallAndRet(errstmt) + let jsProc = gen.newJSProc(getJSParams()) + gen.registerConstructor(jsProc) + return fun + +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; uf: static bool; fun: typed) = + var gen = initGenerator(fun, bfGetter, hasThis = true, jsname = jsname, + unforgeable = uf) + 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 = gen.newJSProc(getJSGetterParams(), 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("", false, fun) + +template jsuffget*(fun: untyped) = + jsfgetn("", true, fun) + +template jsfget*(jsname, fun: untyped) = + jsfgetn(jsname, false, fun) + +template jsuffget*(jsname, fun: untyped) = + jsfgetn(jsname, true, 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; uf: static bool; + staticName: static string; fun: typed) = + var gen = initGenerator(fun, bfFunction, hasThis = true, jsname = jsname, + unforgeable = uf, staticName = staticName) + if gen.minArgs == 0 and not gen.isstatic: + error("Zero-parameter functions are not supported. " & + "(Maybe pass Window or Client?)") + if not gen.isstatic: + 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("", false, "", fun) + +template jsuffunc*(fun: untyped) = + jsfuncn("", true, "", fun) + +template jsfunc*(jsname, fun: untyped) = + jsfuncn(jsname, false, "", fun) + +template jsuffunc*(jsname, fun: untyped) = + jsfuncn(jsname, true, "", fun) + +template jsstfunc*(name, fun: untyped) = + jsfuncn("", false, 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; val: JSValue) = + let opaque {.inject.} = JS_GetOpaque(val, JS_GetClassID(val)) + if opaque != nil: + `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.} + +proc js_illegal_ctor*(ctx: JSContext; this: JSValue; argc: cint; + argv: ptr UncheckedArray[JSValue]): JSValue {.cdecl.} = + return JS_ThrowTypeError(ctx, "Illegal constructor") + +type + JSObjectPragma = object + name: string + varsym: NimNode + unforgeable: bool + + JSObjectPragmas = object + jsget: seq[JSObjectPragma] + jsset: seq[JSObjectPragma] + jsinclude: seq[JSObjectPragma] + +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) + +proc findPragmas(t: NimNode): JSObjectPragmas = + 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] + var pragmas = JSObjectPragmas() + while identDefsStack.len > 0: + let identDefs = identDefsStack.pop() + case identDefs.kind + of nnkRecList: + for child in identDefs.children: + identDefsStack.add(child) + of nnkRecCase: + # Add condition definition + identDefsStack.add(identDefs[0]) + # Add branches + for i in 1 ..< identDefs.len: + identDefsStack.add(identDefs[i].last) + else: + for i in 0 ..< identDefs.len - 2: + let varNode = identDefs[i] + if varNode.kind == nnkPragmaExpr: + var varName = varNode[0] + if varName.kind == nnkPostfix: + # This is a public field. We are skipping the postfix * + varName = varName[1] + let varPragmas = varNode[1] + for varPragma in varPragmas: + let pragmaName = getPragmaName(varPragma) + var op = JSObjectPragma( + name: getStringFromPragma(varPragma).get($varName), + varsym: varName + ) + case pragmaName + of "jsget": pragmas.jsget.add(op) + of "jsset": pragmas.jsset.add(op) + of "jsufget": # LegacyUnforgeable + op.unforgeable = true + pragmas.jsget.add(op) + of "jsgetset": + pragmas.jsget.add(op) + pragmas.jsset.add(op) + of "jsinclude": pragmas.jsinclude.add(op) + return pragmas + +proc nim_finalize_for_js*(obj: pointer) = + for rt in runtimes: + let rtOpaque = rt.getOpaque() + rtOpaque.plist.withValue(obj, v): + let p = v[] + let val = JS_MKPTR(JS_TAG_OBJECT, p) + let classid = JS_GetClassID(val) + rtOpaque.fins.withValue(classid, fin): + fin[](rt, val) + 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) + +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) + {.warning[Deprecated]:off.}: + proc `=destroy`(obj: var U) = + nim_finalize_for_js(addr obj) + +template jsDestructor*(T: typedesc[object]) = + static: + jsDtors.incl($T) + {.warning[Deprecated]:off.}: + proc `=destroy`(obj: var T) = + nim_finalize_for_js(addr obj) + +type RegistryInfo = 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 + ctorImpl: NimNode # definition & body of constructor + ctorFun: NimNode # constructor ident + getset: Table[string, (NimNode, NimNode, bool)] # name -> get, set, uf + 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 + classDef: NimNode # ClassDef ident + tabUnforgeable: NimNode # array of unforgeable function table + tabStatic: NimNode # array of static function table + +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 newRegistryInfo(t: NimNode; name: string): RegistryInfo = + return RegistryInfo( + t: t, + name: name, + classDef: ident("classDef"), + tabList: newNimNode(nnkBracket), + tabUnforgeable: newNimNode(nnkBracket), + tabStatic: newNimNode(nnkBracket), + finName: newNilLit(), + finFun: newNilLit(), + propGetOwnFun: newNilLit(), + propGetFun: newNilLit(), + propSetFun: newNilLit(), + propDelFun: newNilLit(), + propHasFun: newNilLit(), + propNamesFun: newNilLit() + ) + +proc bindConstructor(stmts: NimNode; info: var RegistryInfo): NimNode = + if info.ctorFun != nil: + stmts.add(info.ctorImpl) + return info.ctorFun + return ident("js_illegal_ctor") + +proc registerGetters(stmts: NimNode; info: RegistryInfo; + jsget: seq[JSObjectPragma]) = + let t = info.t + let tname = info.tname + let jsname = info.jsname + for op in jsget: + let node = op.varsym + let fn = op.name + let id = ident($bfGetter & "_" & tname & "_" & fn) + stmts.add(quote do: + proc `id`(ctx: JSContext; this: JSValue): JSValue {.cdecl.} = + when `t` is object: + var arg_0: ptr `t` + else: + var arg_0: `t` + if ctx.fromJSThis(this, arg_0).isNone: + return JS_ThrowTypeError(ctx, + "'%s' called on an object that is not an instance of %s", `fn`, + `jsname`) + when typeof(arg_0.`node`) is object: + return toJSP(ctx, arg_0, arg_0.`node`) + else: + return toJS(ctx, arg_0.`node`) + ) + registerFunction(tname, BoundFunction( + t: bfGetter, + name: fn, + id: id, + unforgeable: op.unforgeable + )) + +proc registerSetters(stmts: NimNode; info: RegistryInfo; + jsset: seq[JSObjectPragma]) = + let t = info.t + let tname = info.tname + let jsname = info.jsname + for op in jsset: + let node = op.varsym + let fn = op.name + let id = ident($bfSetter & "_" & tname & "_" & fn) + stmts.add(quote do: + proc `id`(ctx: JSContext; this: JSValue; val: JSValue): JSValue + {.cdecl.} = + when `t` is object: + var arg_0: ptr `t` + else: + var arg_0: `t` + if ctx.fromJS(this, arg_0).isNone: + return JS_ThrowTypeError(ctx, + "'%s' called on an object that is not an instance of %s", `fn`, + `jsname`) + # We can't just set arg_0.`node` directly, or fromJS may damage it. + var nodeVal: typeof(arg_0.`node`) + if ctx.fromJS(val, nodeVal).isNone: + return JS_EXCEPTION + arg_0.`node` = move(nodeVal) + return JS_DupValue(ctx, val) + ) + registerFunction(tname, bfSetter, fn, id) + +proc bindFunctions(stmts: NimNode; info: var RegistryInfo) = + BoundFunctions.withValue(info.tname, funs): + for fun in funs[].mitems: + var f0 = fun.name + let f1 = fun.id + if fun.name.endsWith("_exceptions"): + fun.name = fun.name.substr(0, fun.name.high - "_exceptions".len) + case fun.t + of bfFunction: + f0 = fun.name + if fun.unforgeable: + info.tabUnforgeable.add(quote do: + JS_CFUNC_DEF_NOCONF(`f0`, 0, cast[JSCFunction](`f1`))) + elif fun.isstatic: + info.tabStatic.add(quote do: + JS_CFUNC_DEF(`f0`, 0, cast[JSCFunction](`f1`))) + else: + info.tabList.add(quote do: + JS_CFUNC_DEF(`f0`, 0, cast[JSCFunction](`f1`))) + of bfConstructor: + info.ctorImpl = fun.ctorBody + if info.ctorFun != nil: + error("Class " & info.tname & " has 2+ constructors.") + info.ctorFun = f1 + of bfGetter: + info.getset.withValue(f0, exv): + exv[0] = f1 + exv[2] = fun.unforgeable + do: + info.getset[f0] = (f1, newNilLit(), fun.unforgeable) + of bfSetter: + info.getset.withValue(f0, exv): + exv[1] = f1 + do: + info.getset[f0] = (newNilLit(), f1, false) + of bfPropertyGetOwn: + if info.propGetFun.kind != nnkNilLit: + error("Class " & info.tname & " has 2+ own property getters.") + info.propGetOwnFun = f1 + of bfPropertyGet: + if info.propGetFun.kind != nnkNilLit: + error("Class " & info.tname & " has 2+ property getters.") + info.propGetFun = f1 + of bfPropertySet: + if info.propSetFun.kind != nnkNilLit: + error("Class " & info.tname & " has 2+ property setters.") + info.propSetFun = f1 + of bfPropertyDel: + if info.propDelFun.kind != nnkNilLit: + error("Class " & info.tname & " has 2+ property deleters.") + info.propDelFun = f1 + of bfPropertyHas: + if info.propHasFun.kind != nnkNilLit: + error("Class " & info.tname & " has 2+ hasprop getters.") + info.propHasFun = f1 + of bfPropertyNames: + if info.propNamesFun.kind != nnkNilLit: + error("Class " & info.tname & " has 2+ propnames getters.") + info.propNamesFun = f1 + of bfFinalizer: + f0 = fun.name + info.finFun = ident(f0) + info.finName = f1 + +proc bindGetSet(stmts: NimNode; info: RegistryInfo) = + for k, (get, set, unforgeable) in info.getset: + if not unforgeable: + info.tabList.add(quote do: JS_CGETSET_DEF(`k`, `get`, `set`)) + else: + info.tabUnforgeable.add(quote do: + JS_CGETSET_DEF_NOCONF(`k`, `get`, `set`)) + +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: JSValue): 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: + rt.getOpaque().destroying = nil + # 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: JSValue): 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 classDef = info.classDef + 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`, + exotic: JSClassExoticMethodsConst(addr exotic) + ) + let `classDef` = JSClassDefConst(addr cd) + ) + else: + endstmts.add(quote do: + var cd {.global.} = JSClassDef( + class_name: `jsname`, + can_destroy: `dfin` + ) + let `classDef` = JSClassDefConst(addr cd) + ) + +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; + errid = Opt[JSErrorEnum].err(); ishtmldda = false): JSClassID = + var stmts = newStmtList() + var info = newRegistryInfo(t, 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!") + let pragmas = findPragmas(t) + stmts.registerGetters(info, pragmas.jsget) + stmts.registerSetters(info, pragmas.jsset) + stmts.bindFunctions(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 classDef = info.classDef + let tname = info.tname + let unforgeable = info.tabUnforgeable + let staticfuns = info.tabStatic + let global = asglobal and not globalparent + endstmts.add(quote do: + `ctx`.newJSClass(`classDef`, `tname`, getTypePtr(`t`), `sctr`, `tabList`, + `parent`, bool(`global`), `nointerface`, `finName`, `namespace`, + `errid`, `unforgeable`, `staticfuns`, `ishtmldda`) + ) + stmts.add(newBlockStmt(endstmts)) + return stmts + +proc getMemoryUsage*(rt: JSRuntime): string = + var m: JSMemoryUsage + JS_ComputeMemoryUsage(rt, m) + template row(title: string; count, size, sz2, cnt2: int64, name: string): + string = + var fv = $(float(sz2) / float(cnt2)) + let i = fv.find('.') + if i != -1: + fv.setLen(i + 1) + else: + fv &= ".0" + title & ": " & $count & " " & $size & " (" & fv & ")" & name & "\n" + template row(title: string; count, size, sz2: int64, name: string): + string = + row(title, count, size, sz2, count, name) + template row(title: string; count, size: int64, name: string): string = + row(title, count, size, size, name) + var s = "" + if m.malloc_count != 0: + s &= row("memory allocated", m.malloc_count, m.malloc_size, "/block") + s &= row("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 &= row("atoms", m.atom_count, m.atom_size, "/atom") + if m.str_count != 0: + s &= row("strings", m.str_count, m.str_size, "/string") + if m.obj_count != 0: + s &= row("objects", m.obj_count, m.obj_size, "/object") & + row("properties", m.prop_count, m.prop_size, m.prop_size, m.obj_count, + "/object") & + row("shapes", m.shape_count, m.shape_size, "/shape") + if m.js_func_count != 0: + s &= row("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" & + row("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 + return s + +proc eval*(ctx: JSContext; s: string; file = "<input>"; + 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 = "<input>"): JSValue = + return ctx.eval(s, file, JS_EVAL_FLAG_COMPILE_ONLY) + +proc compileModule*(ctx: JSContext; s: string; file = "<input>"): 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 |