# JavaScript binding generator. Horrifying, I know. But it works! # Warning: Function overloading is currently not implemented. Though there is a # block dielabel: # ... # around each bound function call, so it shouldn't be too difficult to get it # working. (This would involve generating JS functions in registerType.) # Now for the pragmas: # {.jsctr.} 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. As mentioned before, overloading doesn't work but OR # generics do. Bare objects (returned by value) can't be passed either, for # now. Otherwise, most types should work. #{.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.) #{.jsgetprop.} for property getters. Called when GetOwnProperty would return # nothing. The key should probably be either a string or an integer. #{.jshasprop.} for overriding has_property. Must return a boolean. import macros import options import sets import streams import strformat import strutils import tables import unicode import bindings/quickjs export options export JS_NULL, JS_UNDEFINED, JS_FALSE, JS_TRUE, JS_EXCEPTION, JS_UNINITIALIZED export 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_STRIP, JS_EVAL_FLAG_COMPILE_ONLY export JSRuntime, JSContext, JSValue export JS_GetGlobalObject, JS_FreeValue, JS_IsException, JS_GetPropertyStr, JS_IsFunction, JS_NewCFunctionData, JS_Call type JSContextOpaque* = ref object creg: Table[string, JSClassID] typemap: Table[pointer, JSClassID] ctors: Table[JSClassID, JSValue] #TODO TODO TODO free these gclaz: string sym_iterator: JSAtom sym_asyncIterator: JSAtom sym_toStringTag: JSAtom done: JSAtom next: JSAtom value: JSAtom Array_prototype_values: JSValue JSRuntimeOpaque* = ref object plist: Table[pointer, pointer] flist: seq[seq[JSCFunctionListEntry]] JSFunctionList* = openArray[JSCFunctionListEntry] func getOpaque*(ctx: JSContext): JSContextOpaque = return cast[JSContextOpaque](JS_GetContextOpaque(ctx)) func getOpaque*(rt: JSRuntime): JSRuntimeOpaque = return cast[JSRuntimeOpaque](JS_GetRuntimeOpaque(rt)) var runtimes {.threadVar.}: seq[JSRuntime] proc newJSRuntime*(): JSRuntime = result = JS_NewRuntime() runtimes.add(result) var opaque = new(JSRuntimeOpaque) GC_ref(opaque) JS_SetRuntimeOpaque(result, cast[pointer](opaque)) proc newJSContext*(rt: JSRuntime): JSContext = let ctx = JS_NewContext(rt) var opaque = new(JSContextOpaque) GC_ref(opaque) block: # get well-known symbols and other functions let global = JS_GetGlobalObject(ctx) block: let sym = JS_GetPropertyStr(ctx, global, "Symbol") block: let it = JS_GetPropertyStr(ctx, sym, "iterator") assert JS_IsSymbol(it) opaque.sym_iterator = JS_ValueToAtom(ctx, it) JS_FreeValue(ctx, it) block: let ait = JS_GetPropertyStr(ctx, sym, "asyncIterator") assert JS_IsSymbol(ait) opaque.sym_asyncIterator = JS_ValueToAtom(ctx, ait) JS_FreeValue(ctx, ait) block: let ait = JS_GetPropertyStr(ctx, sym, "toStringTag") assert JS_IsSymbol(ait) opaque.sym_toStringTag = JS_ValueToAtom(ctx, ait) JS_FreeValue(ctx, ait) block: let s = "done" opaque.done = JS_NewAtomLen(ctx, cstring(s), csize_t(s.len)) block: let s = "value" opaque.value = JS_NewAtomLen(ctx, cstring(s), csize_t(s.len)) block: let s = "next" opaque.next = JS_NewAtomLen(ctx, cstring(s), csize_t(s.len)) block: # 2 - JS_CLASS_ARRAY let arrproto = JS_GetClassProto(ctx, 2) opaque.Array_prototype_values = JS_GetPropertyStr(ctx, arrproto, "values") JS_FreeValue(ctx, arrproto) JS_FreeValue(ctx, sym) JS_FreeValue(ctx, global) JS_SetContextOpaque(ctx, cast[pointer](opaque)) return ctx proc newJSContextRaw*(rt: JSRuntime): JSContext = result = JS_NewContextRaw(rt) func getJSValue(ctx: JSContext, argv: ptr JSValue, i: int): JSValue {.inline.} = cast[ptr JSValue](cast[int](argv) + i * sizeof(JSValue))[] func getClass*(ctx: JSContext, class: string): JSClassID = # This function *should* never fail. ctx.getOpaque().creg[class] func findClass*(ctx: JSContext, class: string): Option[JSClassID] = let opaque = ctx.getOpaque() if class in opaque.creg: return some(opaque.creg[class]) return none(JSClassID) func newJSCFunction*(ctx: JSContext, name: string, fun: JSCFunction, argc: int = 0, proto = JS_CFUNC_generic, magic = 0): JSValue = return JS_NewCFunction2(ctx, fun, cstring(name), cint(argc), proto, cint(magic)) proc free*(ctx: var JSContext) = var opaque = ctx.getOpaque() if opaque != nil: JS_FreeAtom(ctx, opaque.sym_iterator) JS_FreeAtom(ctx, opaque.sym_asyncIterator) JS_FreeAtom(ctx, opaque.sym_toStringTag) JS_FreeAtom(ctx, opaque.done) JS_FreeAtom(ctx, opaque.next) JS_FreeValue(ctx, opaque.Array_prototype_values) GC_unref(opaque) JS_FreeContext(ctx) ctx = nil proc free*(rt: var JSRuntime) = let opaque = rt.getOpaque() GC_unref(opaque) JS_FreeRuntime(rt) runtimes.del(runtimes.find(rt)) rt = nil proc setOpaque*[T](ctx: JSContext, val: JSValue, opaque: T) = let rt = JS_GetRuntime(ctx) let rtOpaque = rt.getOpaque() let p = JS_VALUE_GET_PTR(val) let header = cast[ptr JSRefCountHeader](p) inc header.ref_count # add jsvalue reference rtOpaque.plist[cast[pointer](opaque)] = p JS_SetOpaque(val, cast[pointer](opaque)) func isGlobal*(ctx: JSContext, class: string): bool = assert class != "" return ctx.getOpaque().gclaz == class # A hack to retrieve a given val's class id. func getClassID*(val: JSValue): JSClassID = const index = sizeof(cint) + # gc_ref_count sizeof(uint8) + # gc_mark sizeof(uint8) # bit field return cast[ptr uint16](cast[int](JS_VALUE_GET_PTR(val)) + index)[] func getOpaque*(ctx: JSContext, val: JSValue, class: string): pointer = # Unfortunately, we can't change the global object's class. #TODO: or maybe we can, but I'm afraid of breaking something. # This needs further investigation. if ctx.isGlobal(class): let global = JS_GetGlobalObject(ctx) let opaque = JS_GetOpaque(global, 1) # JS_CLASS_OBJECT JS_FreeValue(ctx, global) return opaque if JS_VALUE_GET_TAG(val) == JS_TAG_OBJECT: return JS_GetOpaque(val, val.getClassID()) return nil proc setInterruptHandler*(rt: JSRuntime, cb: JSInterruptHandler, opaque: pointer = nil) = JS_SetInterruptHandler(rt, cb, opaque) func toString*(ctx: JSContext, val: JSValue): Option[string] = var plen: csize_t let outp = JS_ToCStringLen(ctx, addr plen, val) # cstring if outp != nil: var ret = newString(plen) for i in 0.. 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) JS_SetPropertyFunctionList(ctx, proto, addr rtOpaque.flist[^1][0], 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.Array_prototype_values) assert JS_SetProperty(ctx, proto, ctxOpaque.sym_iterator, val) == 1 assert JS_SetProperty(ctx, proto, ctxOpaque.sym_toStringTag, JS_NewString(ctx, cdef.class_name)) == 1 JS_SetClassProto(ctx, result, proto) if asglobal: let global = JS_GetGlobalObject(ctx) assert ctxOpaque.gclaz == "" ctxOpaque.gclaz = tname if JS_SetPrototype(ctx, global, proto) != 1: raise newException(Defect, "Failed to set global prototype: " & $cdef.class_name) JS_FreeValue(ctx, global) let jctor = ctx.newJSCFunction($cdef.class_name, ctor, 0, JS_CFUNC_constructor) JS_SetConstructor(ctx, jctor, proto) ctxOpaque.ctors[result] = JS_DupValue(ctx, jctor) if not nointerface: let global = JS_GetGlobalObject(ctx) ctx.setProperty(global, $cdef.class_name, jctor) JS_FreeValue(ctx, global) type FuncParam = tuple[name: string, t: NimNode, val: Option[NimNode], generic: Option[NimNode]] func getMinArgs(params: seq[FuncParam]): int = for i in 0.. 1: return none(char) return some(s.get[0]) elif T is Rune: let s = toString(ctx, val) if s.isNone: return none(Rune) var i = 0 var r: Rune fastRuneAt(s.get, i, r) if i < s.get.len: return none(Rune) return some(r) elif T is (proc): return fromJSFunction1[typeof(unpackReturnType(T)), typeof(unpackArg0(T))](ctx, val) elif typeof(result.unsafeGet) is Option: # unwrap let res = fromJS[typeof(result.get.get)](ctx, val) if res.isnone: return none(T) return some(res) elif T is seq: return fromJSSeq[typeof(result.get.items)](ctx, val) elif T is set: return fromJSSet[typeof(result.get.items)](ctx, val) elif T is tuple: return fromJSTuple[T](ctx, val) elif T is bool: let ret = JS_ToBool(ctx, val) if ret == -1: # exception return none(T) if ret == 0: return some(false) return some(true) elif typeof(result.get) is Table: return fromJSTable[typeof(result.get.keys), typeof(result.get.values)](ctx, val) elif T is SomeInteger: if JS_IsNumber(val): return fromJSInt[T](ctx, val) elif T is SomeFloat: if JS_IsNumber(val): let f64: float64 if JS_ToFloat64(ctx, addr f64, val) < 0: return none(T) return some(cast[T](f64)) elif T is enum: #TODO implement enum handling... if JS_IsException(val): return none(T) let s = toString(ctx, val) if s.isnone: return none(T) try: return some(parseEnum[T](s.get)) except ValueError: JS_ThrowTypeError(ctx, "`%s' is not a valid value for enumeration %s", cstring(s.get), $T) return none(T) elif T is JSValue: return some(val) elif T is object: doAssert false, "Dictionary case has not been implemented yet!" #TODO TODO TODO implement dictionary case return none(T) else: if JS_IsException(val): return none(T) let op = cast[T](getOpaque(ctx, val, $T)) if op == nil: JS_ThrowTypeError(ctx, "Value is not an instance of %s", $T) return none(T) return some(op) const JS_ATOM_TAG_INT = cuint(1u32 shl 31) func JS_IsNumber(v: JSAtom): JS_BOOL = return (cast[cuint](v) and JS_ATOM_TAG_INT) != 0 func fromJS[T: string|uint32](ctx: JSContext, atom: JSAtom): Option[T] = when T is SomeNumber: if JS_IsNumber(atom): return some(T(cast[uint32](atom) and (not JS_ATOM_TAG_INT))) else: let val = JS_AtomToValue(ctx, atom) return fromJS[T](ctx, val) proc getJSFunction*[T, U](ctx: JSContext, val: JSValue): Option[(proc(x: T): Option[U])] = return fromJS[(proc(x: T): Option[U])](ctx, val) func toJSString(ctx: JSContext, str: string): JSValue = return JS_NewString(ctx, cstring(str)) func toJSInt(ctx: JSContext, n: SomeInteger): JSValue = when n is int: when sizeof(int) <= sizeof(int32): return JS_NewInt32(ctx, int32(n)) else: return JS_NewInt64(ctx, n) elif n is uint: when sizeof(uint) <= sizeof(uint32): return JS_NewUint32(ctx, n) else: return JS_NewUint64(ctx, n) elif n is int32: return JS_NewInt32(ctx, n) elif n is int64: return JS_NewInt64(ctx, n) elif n is uint32: return JS_NewUint32(ctx, n) elif n is uint64: return JS_NewUint64(ctx, n) func toJSNumber(ctx: JSContext, n: SomeNumber): JSValue = when n is SomeInteger: return toJSInt(ctx, n) else: return JS_NewFloat64(ctx, n) func toJSBool(ctx: JSContext, b: bool): JSValue = return JS_NewBool(ctx, b) proc getTypePtr[T](x: T): pointer = when T is RootRef or T is pointer: # I'm so sorry. # (This dereferences the object's first member, m_type. Probably.) return cast[ptr pointer](x)[] else: return getTypeInfo(x) func toJSObject[T](ctx: JSContext, obj: T): JSValue = let op = JS_GetRuntime(ctx).getOpaque() let p = cast[pointer](obj) if p in op.plist: # a JSValue already points to this object. return JS_DupValue(ctx, JS_MKPTR(JS_TAG_OBJECT, op.plist[p])) let clazz = ctx.getOpaque().typemap[getTypePtr(obj)] let jsObj = JS_NewObjectClass(ctx, clazz) setOpaque(ctx, jsObj, obj) return jsObj proc toJS*[T](ctx: JSContext, obj: T): JSValue = when T is string: return ctx.toJSString(obj) elif T is Rune: return ctx.toJSString($obj) elif T is SomeNumber: return ctx.toJSNumber(obj) elif T is bool: return ctx.toJSBool(obj) elif T is Table: result = JS_NewObject(ctx) if not JS_IsException(result): for k, v in obj: setProperty(ctx, result, k, toJS(ctx, v)) elif T is Option: if obj.issome: return toJS(ctx, obj.get) return JS_NULL elif T is seq: let a = JS_NewArray(ctx) if not JS_IsException(a): for i in 0..obj.high: let j = toJS(ctx, obj[i]) if JS_IsException(j): return j if JS_DefinePropertyValueInt64(ctx, a, int64(i), j, JS_PROP_C_W_E or JS_PROP_THROW) < 0: return JS_EXCEPTION return a elif T is enum: return toJS(ctx, int(obj)) elif T is JSValue: return obj else: if obj == nil: return JS_NULL return ctx.toJSObject(obj) type JS_Error = object of CatchableError JS_SyntaxError* = object of JS_Error JS_TypeError* = object of JS_Error JS_ReferenceError* = object of JS_Error JS_RangeError* = object of JS_Error JS_InternalError* = object of JS_Error JSFuncGenerator = object t: BoundFunctionType original: NimNode copied: NimNode thisname: Option[string] funcName: string generics: Table[string, seq[NimNode]] funcParams: seq[FuncParam] thisType: string returnType: Option[NimNode] newName: string newBranchList: seq[NimNode] errval: NimNode # JS_EXCEPTION or -1 dielabel: NimNode # die: didn't match parameters, but could still match other ones jsFunCallLists: seq[NimNode] jsFunCallList: NimNode jsFunCall: NimNode jsCallAndRet: NimNode minArgs: int i: int # nim parameters accounted for j: int # js parameters accounted for (not including fix ones, e.g. `this') res: NimNode BoundFunction = object t: BoundFunctionType name: string id: NimNode magic: uint16 BoundFunctionType = enum FUNCTION = "js_func" CONSTRUCTOR = "js_ctor" GETTER = "js_get" SETTER = "js_set" PROPERTY_GET = "js_prop_get" PROPERTY_HAS = "js_prop_has" var BoundFunctions {.compileTime.}: Table[string, seq[BoundFunction]] proc getGenerics(fun: NimNode): Table[string, seq[NimNode]] = var node = fun.findChild(it.kind == nnkBracket) if node.kind == nnkNilLit: return # no bracket node = node.findChild(it.kind == nnkGenericParams) if node.kind == nnkNilLit: return # no generics node = node.findChild(it.kind == nnkIdentDefs) var stack: seq[NimNode] for i in countdown(node.len - 1, 0): stack.add(node[i]) var gen_name: NimNode var gen_types: seq[NimNode] template add_gen = if gen_name != nil: assert gen_types.len != 0 result[gen_name.strVal] = gen_types gen_types.setLen(0) while stack.len > 0: let node = stack.pop() case node.kind of nnkIdent: add_gen gen_name = node of nnkSym: assert gen_name != nil gen_types.add(node) of nnkInfix: assert node[0].eqIdent(ident("|")) or node[0].eqIdent(ident("or")), "Only OR generics are supported." for i in countdown(node.len - 1, 1): stack.add(node[i]) # except infix ident of nnkBracketExpr: gen_types.add(node) else: discard add_gen 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.. 0 and g[0] == Table.getType(): tableg = some(g) elif g.typekind == ntySequence: seqg = some(g) elif g == string.getTypeInst(): hasString = true elif g == JSValue.getTypeInst(): hasJSObject = true elif g == bool.getTypeInst(): hasBoolean = true elif g == int.getTypeInst(): #TODO should be SomeNumber numg = some(g) else: error("Type not supported yet") # 10. If Type(V) is Object, then: # Sequence: if seqg.issome: let query = quote do: ( let o = `val` JS_IsObject(o) and ( let prop = JS_GetProperty(ctx, o, ctx.getOpaque().sym_iterator) # prop can't be exception (throws_ref_error is 0 and tag is object) let ret = not JS_IsUndefined(prop) JS_FreeValue(ctx, prop) ret ) ) let a = seqg.get[1] gen.addUnionParamBranch(query, quote do: let `s` = fromJS_or_die(seq[`a`], ctx, `val`, `ev`, `dl`), fallback) # Record: if tableg.issome: let a = tableg.get[1] let b = tableg.get[2] let query = quote do: JS_IsObject(`val`) gen.addUnionParamBranch(query, quote do: let `s` = fromJS_or_die(Table[`a`, `b`], ctx, `val`, `ev`, `dl`), fallback) # Object (JSObject variant): #TODO non-JS objects (i.e. ref object) if hasJSObject: let query = quote do: JS_IsObject(`val`) gen.addUnionParamBranch(query, quote do: let `s` = fromJS_or_die(JSValue, ctx, `val`, `ev`, `dl`), fallback) # 11. If Type(V) is Boolean, then: if hasBoolean: let query = quote do: JS_IsBool(`val`) gen.addUnionParamBranch(query, quote do: let `s` = fromJS_or_die(bool, ctx, `val`, `ev`, `dl`), fallback) # 12. If Type(V) is Number, then: if numg.isSome: let ng = numg.get let query = quote do: JS_IsNumber(`val`) gen.addUnionParamBranch(query, quote do: let `s` = fromJS_or_die(`ng`, ctx, `val`, `ev`, `dl`), fallback) # 14. If types includes a string type, then return the result of converting V # to that type. if hasString: gen.addParam2(s, string.getType(), quote do: `val`, fallback) # 16. If types includes a numeric type, then return the result of converting # V to that numeric type. elif numg.isSome: gen.addParam2(s, numg.get.getType(), quote do: `val`, fallback) # 17. If types includes boolean, then return the result of converting V to # boolean. elif hasBoolean: gen.addParam2(s, bool.getType(), quote do: `val`, fallback) # 19. Throw a TypeError. else: gen.addParam2(s, string.getType(), quote do: if true: discard JS_ThrowTypeError(ctx, "No match for union type") return `ev` JS_NULL, fallback) for branch in gen.newBranchList: gen.jsFunCallLists.add(branch) gen.newBranchList.setLen(0) proc addUnionParam(gen: var JSFuncGenerator, tt: NimNode, s: NimNode, fallback: NimNode = nil) = let j = gen.j gen.addUnionParam0(tt, s, quote do: getJSValue(ctx, argv, `j`), fallback) proc addFixParam(gen: var JSFuncGenerator, name: string) = let s = ident("arg_" & $gen.i) let t = gen.funcParams[gen.i][1] let id = ident(name) if t.typeKind == ntyGenericParam: gen.addUnionParam0(t, s, id) else: gen.addParam2(s, t, id) if gen.jsFunCall != nil: gen.jsFunCall.add(s) inc gen.i proc addRequiredParams(gen: var JSFuncGenerator) = let minArgs = gen.funcParams.getMinArgs() while gen.i < minArgs: let s = ident("arg_" & $gen.i) let tt = gen.funcParams[gen.i][1] if tt.typeKind == ntyGenericParam: gen.addUnionParam(tt, s) else: gen.addValueParam(s, tt) if gen.jsFunCall != nil: 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 let s = ident("arg_" & $gen.i) let tt = gen.funcParams[gen.i][1] if tt.typeKind == varargs.getType().typeKind: # pray it's not a generic... let vt = tt[1].getType() for i in 0..gen.jsFunCallLists.high: gen.jsFunCallLists[i].add(newLetStmt(s, quote do: ( var valist: seq[`vt`] for i in `j`..= 0 result = newStmtList() if isva: result.add(quote do: if argc < `ma`: return JS_ThrowTypeError(ctx, "At least %d arguments required, but only %d passed", `ma`, argc) ) if gen.thisname.isSome: let tn = ident(gen.thisname.get) let ev = gen.errval result.add(quote do: if not (JS_IsUndefined(`tn`) or ctx.isGlobal(`tt`)) and not isInstanceOf(ctx, `tn`, `tt`): # undefined -> global. discard JS_ThrowTypeError(ctx, "'%s' called on an object that is not an instance of %s", `fn`, `tt`) return `ev` ) if gen.funcName in js_errors: var tryWrap = newNimNode(nnkTryStmt) tryWrap.add(gen.jsCallAndRet) for error in js_errors[gen.funcName]: let ename = ident(error) var exceptBranch = newNimNode(nnkExceptBranch) let eid = ident("e") exceptBranch.add(newNimNode(nnkInfix).add(ident("as"), ename, eid)) let throwName = ident("JS_Throw" & error.substr("JS_".len)) exceptBranch.add(quote do: return `throwName`(ctx, "%s", cstring(`eid`.msg))) tryWrap.add(exceptBranch) gen.jsCallAndRet = tryWrap 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")) result = newProc(ident(gen.newName), params, jsBody, pragmas = jsPragmas) gen.res = result # WARNING: for now, this only works correctly when the .jserr pragma was # declared on the parent function. # Note: this causes the entire nim function body to be inlined inside the JS # interface function. #TODO: implement actual inlining (so we can e.g. get rid of JS_Error, use format strings, etc.) macro JS_ERR*(a: typed, b: string) = result = quote do: block when_js: raise newException(`a`, `b`) proc setupGenerator(fun: NimNode, t: BoundFunctionType, thisname = some("this")): JSFuncGenerator = result.t = t result.funcName = $fun[0] if result.funcName == "$": # stringifier result.funcName = "toString" result.generics = getGenerics(fun) result.funcParams = getParams(fun) result.returnType = getReturn(fun) result.minArgs = result.funcParams.getMinArgs() result.original = fun result.thisname = thisname if t in {PROPERTY_GET, PROPERTY_HAS}: result.errval = quote do: cint(-1) else: result.errval = quote do: JS_EXCEPTION result.dielabel = ident("ondie") result.jsFunCallList = newStmtList() result.jsFunCallLists.add(result.jsFunCallList) result.jsFunCall = newCall(fun[0]) if thisname.isSome: result.thisType = $result.funcParams[0][1] result.newName = $t & "_" & result.thisType & "_" & result.funcName else: if result.returnType.get.kind == nnkRefTy: result.thisType = result.returnType.get[0].strVal else: result.thisType = result.returnType.get.strVal result.newName = $t & "_" & result.funcName # this might be pretty slow... #TODO ideally we wouldn't need separate functions at all. Not sure how that # could be achieved, maybe using options? proc rewriteExceptions(gen: var JSFuncGenerator, errors: var seq[string], node: NimNode) = for i in countdown(node.len - 1, 0): let c = node[i] if c.kind == nnkCommand and c[0].eqIdent ident("JS_ERR"): if gen.copied == nil: gen.copied = copy(gen.original) if gen.returnType.isSome: node[i] = quote do: zeroMem(addr result, sizeof(result)) return else: node[i] = quote do: return if c[1].strVal notin errors: errors.add(c[1].strVal) elif c.len > 0: gen.rewriteExceptions(errors, c) proc rewriteExceptions(gen: var JSFuncGenerator) = let ostmts = gen.original.findChild(it.kind == nnkStmtList) var errors: seq[string] gen.rewriteExceptions(errors, ostmts) assert gen.copied != nil var name: string if gen.copied[0].kind == nnkIdent: name = gen.copied[0].strVal elif gen.copied[0].kind == nnkPostfix: name = gen.copied[0][1].strVal else: error("No JS_ERR statement found in proc with jserr pragma.") name &= "_exceptions" gen.copied[0] = ident(name) js_errors[name] = errors macro jserr*(fun: untyped) = var gen: JSFuncGenerator gen.original = fun gen.rewriteExceptions() var pragma = gen.original.findChild(it.kind == nnkPragma) for i in 0.. 1: # References to this value still exist in JS, so we # * copy the opaque's value # * increase the new value's refcount by 1 # * set the new value as the new opaque # * add the new value to the pointer table # Now it's on JS to decrement the new object's refcount. # (Yeah, kind of an ugly hack. But it starts to look better when # the alternative is writing a cycle collector...) let newop = new(T) newop[] = obj[] GC_ref(newop) let np = cast[pointer](newop) JS_SetOpaque(val, np) rtOpaque.plist[np] = p else: # This was the last reference to the JS value. # Clear val's opaque so our refcount isn't decreased again. JS_SetOpaque(val, nil) tables.del(rtOpaque.plist, cast[pointer](obj)) # Decrement jsvalue's refcount. This is needed in both cases to # trigger the JS finalizer and free the JS value. JS_FreeValueRT(rt, val) proc js_illegal_ctor*(ctx: JSContext, this: JSValue, argc: cint, argv: ptr JSValue): JSValue {.cdecl.} = return JS_ThrowTypeError(ctx, "Illegal constructor") type JSObjectPragmas = object jsget: seq[NimNode] jsset: seq[NimNode] proc findPragmas(t: NimNode): JSObjectPragmas = let typ = t.getTypeInst()[1] # The type, as declared. var impl = typ.getTypeImpl() # ref t assert impl.kind == nnkRefTy, "Only ref nodes are supported..." impl = impl[0].getImpl() # stolen from std's macros.customPragmaNode var identDefsStack = newSeq[NimNode](impl[2].len) for i in 0.. 0: var 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 - 3: 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] for pragma in varNode[1]: case $pragma of "jsget": result.jsget.add(varName) of "jsset": result.jsset.add(varName) macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = false, nointerface = false, name: static string = ""): JSClassID = result = newStmtList() let tname = t.strVal # the nim type's name. let name = if name == "": tname else: name # possibly a different name, e.g. Buffer for Container var sctr = ident("js_illegal_ctor") var sfin = ident("js_" & t.strVal & "ClassFin") # constructor var ctorFun: NimNode var ctorImpl: NimNode # generic property getter (e.g. attribute["id"]) var propGetFun = newNilLit() var propHasFun = newNilLit() # property setters/getters declared on classes (with jsget, jsset) var setters, getters: Table[string, NimNode] let tabList = newNimNode(nnkBracket) let pragmas = findPragmas(t) for node in pragmas.jsget: let id = ident($GETTER & "_" & t.strVal & "_" & $node) let fn = $node result.add(quote do: proc `id`(ctx: JSContext, this: JSValue): JSValue {.cdecl.} = if not (JS_IsUndefined(this) or ctx.isGlobal(`tname`)) and not ctx.isInstanceOf(this, `tname`): # undefined -> global. return JS_ThrowTypeError(ctx, "'%s' called on an object that is not an instance of %s", `fn`, `name`) let arg_0 = fromJS_or_return(`t`, ctx, this) return toJS(ctx, arg_0.`node`) ) registerFunction(t.strVal, GETTER, fn, id) for node in pragmas.jsset: let id = ident($SETTER & "_" & t.strVal & "_" & $node) let fn = $node result.add(quote do: proc `id`(ctx: JSContext, this: JSValue, val: JSValue): JSValue {.cdecl.} = if not (JS_IsUndefined(this) or ctx.isGlobal(`tname`)) and not ctx.isInstanceOf(this, `tname`): # undefined -> global. return JS_ThrowTypeError(ctx, "'%s' called on an object that is not an instance of %s", `fn`, `name`) let arg_0 = fromJS_or_return(`t`, ctx, this) let arg_1 = val arg_0.`node` = fromJS_or_return(typeof(arg_0.`node`), ctx, arg_1) return JS_DupValue(ctx, arg_1) ) registerFunction(t.strVal, SETTER, fn, id) if t.strVal in BoundFunctions: for fun in BoundFunctions[t.strVal].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 FUNCTION: f0 = fun.name tabList.add(quote do: JS_CFUNC_DEF(`f0`, 0, cast[JSCFunction](`f1`))) of CONSTRUCTOR: ctorImpl = js_funcs[$f0].res if ctorFun != nil: error("Class " & t.strVal & " has 2+ constructors.") ctorFun = f1 of GETTER: getters[f0] = f1 of SETTER: setters[f0] = f1 of PROPERTY_GET: propGetFun = f1 of PROPERTY_HAS: propHasFun = f1 for k, v in getters: if k in setters: let s = setters[k] tabList.add(quote do: JS_CGETSET_DEF(`k`, `v`, `s`)) else: tabList.add(quote do: JS_CGETSET_DEF(`k`, `v`, nil)) for k, v in setters: if k notin getters: tabList.add(quote do: JS_CGETSET_DEF(`k`, nil, `v`)) if ctorFun != nil: sctr = ctorFun result.add(ctorImpl) result.add(quote do: proc `sfin`(rt: JSRuntime, val: JSValue) {.cdecl.} = let opaque = JS_GetOpaque(val, val.getClassID()) if opaque != nil: # This means the nim value is no longer referenced by anything but this # JSValue. Meaning we can just unref and remove it from the pointer # table. GC_unref(cast[`t`](opaque)) let rtOpaque = rt.getOpaque() rtOpaque.plist.del(opaque) ) let endstmts = newStmtList() let cdname = "classDef" & name let classDef = ident("classDef") if propGetFun.kind != nnkNilLit or propHasFun.kind != nnkNilLit: endstmts.add(quote do: # YES! IT COMPILES! AHAHAHAHAHAHA {.emit: [""" static JSClassExoticMethods exotic = { .get_own_property = """, `propGetFun`, """, .has_property = """, `propHasFun`, """ }; static JSClassDef """, `cdname`, """ = { """, "\"", `name`, "\"", """, .finalizer = """, `sfin`, """, .exotic = &exotic };""" ].} var `classDef`: JSClassDefConst {.emit: [ `classDef`, " = &", `cdname`, ";" ].} ) else: endstmts.add(quote do: const cd = JSClassDef(class_name: `name`, finalizer: `sfin`) let `classDef` = JSClassDefConst(unsafeAddr cd)) endStmts.add(quote do: # See the definition of `new': # > **Note**: # > The `finalizer` refers to the type `T`, not to the object! # > This means that for each object of type `T` the finalizer will be # > called! # We exploit this by setting a finalizer here, which can then unregister # any associated JS object from all relevant runtimes. var x: `t` new(x, nim_finalize_for_js) `ctx`.newJSClass(`classDef`, `tname`, `sctr`, `tabList`, getTypePtr(x), `parent`, `asglobal`, `nointerface`) ) result.add(newBlockStmt(endstmts)) proc getMemoryUsage*(rt: JSRuntime): string = var m: JSMemoryUsage JS_ComputeMemoryUsage(rt, addr m) result = fmt""" memory allocated: {m.malloc_count} {m.malloc_size} ({float(m.malloc_size)/float(m.malloc_count):.1f}/block) memory used: {m.memory_used_count} {m.memory_used_size} ({float(m.malloc_size-m.memory_used_size)/float(m.memory_used_count):.1f} average slack) atoms: {m.atom_count} {m.atom_size} ({float(m.atom_size)/float(m.atom_count):.1f}/atom) strings: {m.str_count} {m.str_size} ({float(m.str_size)/float(m.str_count):.1f}/string) objects: {m.obj_count} {m.obj_size} ({float(m.obj_size)/float(m.obj_count):.1f}/object) properties: {m.prop_count} {m.prop_size} ({float(m.prop_size)/float(m.obj_count):.1f}/object) shapes: {m.shape_count} {m.shape_size} ({float(m.shape_size)/float(m.shape_count):.1f}/shape) js functions: {m.js_func_count} {m.js_func_size} ({float(m.js_func_size)/float(m.js_func_count):.1f}/function) native functions: {m.c_func_count} arrays: {m.array_count} fast arrays: {m.fast_array_count} fast array elements: {m.fast_array_elements} {m.fast_array_elements*sizeof(JSValue)} ({float(m.fast_array_elements)/float(m.fast_array_count):.1f}) binary objects: {m.binary_object_count} {m.binary_object_size}""" proc eval*(ctx: JSContext, s: string, file: string, eval_flags: int): JSValue = return JS_Eval(ctx, cstring(s), cint(s.len), cstring(file), cint(eval_flags))