diff options
author | bptato <nincsnevem662@gmail.com> | 2024-05-03 01:41:38 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-05-03 01:58:12 +0200 |
commit | 970378356d0d7239b332baa37470455391b5e6e4 (patch) | |
tree | 87d93162295b12652137193982c5b3c88e1a3758 /src | |
parent | c48f2caedabbcda03724c43935f4175aac3ecf90 (diff) | |
download | chawan-970378356d0d7239b332baa37470455391b5e6e4.tar.gz |
js: fix various leaks etc.
Previously we didn't actually free the main JS runtime, probably because you can't do this without first waiting for JS to unwind the stack. (This has the unfortunate effect that code now *can* run after quit(). TODO: find a fix for this.) This isn't a huge problem per se, we only have one of these and the OS can clean it up. However, it also disabled the JS_FreeRuntime leak check, which resulted in sieve-like behavior (manual refcounting is a pain). So now we choose the other tradeoff: quit no longer runs exitnow, but it waits for the event loop to run to the end and only then exits the browser. Then, before exit we free the JS context & runtime, and also all JS values allocated by config. Fixes: * fix `ad' flag not being set for just one siteconf/omnirule * fix various leaks (since leak check is enabled now) * use ptr UncheckedArray[JSValue] for QJS bindings that take an array * allow JSAtom in jsgetprop etc., also disallow int types other than uint32 * do not set a destructor for globals
Diffstat (limited to 'src')
-rw-r--r-- | src/bindings/quickjs.nim | 25 | ||||
-rw-r--r-- | src/config/config.nim | 9 | ||||
-rw-r--r-- | src/config/toml.nim | 3 | ||||
-rw-r--r-- | src/html/dom.nim | 37 | ||||
-rw-r--r-- | src/html/env.nim | 6 | ||||
-rw-r--r-- | src/html/event.nim | 17 | ||||
-rw-r--r-- | src/io/dynstream.nim | 6 | ||||
-rw-r--r-- | src/js/console.nim | 4 | ||||
-rw-r--r-- | src/js/domexception.nim | 2 | ||||
-rw-r--r-- | src/js/fromjs.nim | 112 | ||||
-rw-r--r-- | src/js/javascript.nim | 94 | ||||
-rw-r--r-- | src/js/jsutils.nim | 9 | ||||
-rw-r--r-- | src/js/opaque.nim | 91 | ||||
-rw-r--r-- | src/js/tojs.nim | 39 | ||||
-rw-r--r-- | src/local/client.nim | 77 | ||||
-rw-r--r-- | src/local/pager.nim | 44 |
16 files changed, 319 insertions, 256 deletions
diff --git a/src/bindings/quickjs.nim b/src/bindings/quickjs.nim index 164f7dad..7a3eee54 100644 --- a/src/bindings/quickjs.nim +++ b/src/bindings/quickjs.nim @@ -80,7 +80,8 @@ type JSCFunction* = proc(ctx: JSContext; this_val: JSValue; argc: cint; argv: ptr UncheckedArray[JSValue]): JSValue {.cdecl.} JSCFunctionData* = proc(ctx: JSContext; this_val: JSValue; argc: cint; - argv: ptr JSValue; magic: cint; func_data: ptr JSValue): JSValue {.cdecl.} + argv: ptr UncheckedArray[JSValue]; magic: cint; + func_data: ptr UncheckedArray[JSValue]): JSValue {.cdecl.} JSGetterFunction* = proc(ctx: JSContext; this_val: JSValue): JSValue {.cdecl.} JSSetterFunction* = proc(ctx: JSContext; this_val, val: JSValue): JSValue {.cdecl.} @@ -90,7 +91,7 @@ type magic: cint): JSValue {.cdecl.} JSInterruptHandler* = proc(rt: JSRuntime; opaque: pointer): cint {.cdecl.} JSClassID* = uint32 - JSAtom* = uint32 + JSAtom* = distinct uint32 JSClassFinalizer* = proc(rt: JSRuntime; val: JSValue) {.cdecl.} JSClassCheckDestroy* = proc(rt: JSRuntime; val: JSValue): JS_BOOL {.cdecl.} JSClassGCMark* = proc(rt: JSRuntime; val: JSValue; mark_func: JS_MarkFunc) @@ -100,8 +101,8 @@ type module_name: cstringConst; opaque: pointer): cstring {.cdecl.} JSModuleLoaderFunc* = proc(ctx: JSContext; module_name: cstringConst, opaque: pointer): JSModuleDef {.cdecl.} - JSJobFunc* = proc(ctx: JSContext; argc: cint; argv: ptr JSValue): JSValue - {.cdecl.} + JSJobFunc* = proc(ctx: JSContext; argc: cint; + argv: ptr UncheckedArray[JSValue]): JSValue {.cdecl.} JSGCObjectHeader* {.importc, header: qjsheader.} = object JSFreeArrayBufferDataFunc* = proc(rt: JSRuntime; opaque, p: pointer) {.cdecl.} @@ -185,7 +186,7 @@ type base: cint JSCFunctionListEntryPropList = object - tab: ptr JSCFunctionListEntry + tab: ptr UncheckedArray[JSCFunctionListEntry] len: cint JSCFunctionListEntryU* {.union.} = object @@ -372,8 +373,8 @@ proc JS_NewObjectClass*(ctx: JSContext; class_id: JSClassID): JSValue proc JS_NewObjectProto*(ctx: JSContext; proto: JSValue): JSValue proc JS_NewObjectProtoClass*(ctx: JSContext; proto: JSValue; class_id: JSClassID): JSValue -proc JS_NewPromiseCapability*(ctx: JSContext; resolving_funcs: ptr JSValue): - JSValue +proc JS_NewPromiseCapability*(ctx: JSContext; + resolving_funcs: ptr UncheckedArray[JSValue]): JSValue proc JS_SetOpaque*(obj: JSValue; opaque: pointer) proc JS_GetOpaque*(obj: JSValue; class_id: JSClassID): pointer proc JS_GetOpaque2*(ctx: JSContext; obj: JSValue; class_id: JSClassID): pointer @@ -422,7 +423,7 @@ proc JS_FreeAtomRT*(rt: JSRuntime; atom: JSAtom) proc JS_NewCFunction2*(ctx: JSContext; cfunc: JSCFunction; name: cstring; length: cint; proto: JSCFunctionEnum; magic: cint): JSValue proc JS_NewCFunctionData*(ctx: JSContext; cfunc: JSCFunctionData; - length, magic, data_len: cint; data: ptr JSValue): JSValue + length, magic, data_len: cint; data: ptr UncheckedArray[JSValue]): JSValue proc JS_NewCFunction*(ctx: JSContext; cfunc: JSCFunction; name: cstring; length: cint): JSValue @@ -453,13 +454,13 @@ proc JS_GetOwnPropertyNames*(ctx: JSContext; proc JS_GetOwnProperty*(ctx: JSContext; desc: ptr JSPropertyDescriptor; obj: JSValue; prop: JSAtom): cint proc JS_Call*(ctx: JSContext; func_obj, this_obj: JSValue; argc: cint; - argv: ptr JSValue): JSValue + argv: ptr UncheckedArray[JSValue]): JSValue proc JS_NewObjectFromCtor*(ctx: JSContext; ctor: JSValue; class_id: JSClassID): JSValue proc JS_Invoke*(ctx: JSContext; this_obj: JSValue; atom: JSAtom; argc: cint; - argv: ptr JSValue): JSValue + argv: ptr UncheckedArray[JSValue]): JSValue proc JS_CallConstructor*(ctx: JSContext; func_obj: JSValue; argc: cint; - argv: ptr JSValue): JSValue + argv: ptr UncheckedArray[JSValue]): JSValue proc JS_DefineProperty*(ctx: JSContext; this_obj: JSValue; prop: JSAtom; val, getter, setter: JSValue; flags: cint): cint @@ -544,7 +545,7 @@ proc JS_GetImportMeta*(ctx: JSContext; m: JSModuleDef): JSValue proc JS_GetModuleName*(ctx: JSContext; m: JSModuleDef): JSAtom proc JS_EnqueueJob*(ctx: JSContext; job_func: JSJobFunc; argc: cint; - argv: ptr JSValue): cint + argv: ptr UncheckedArray[JSValue]): cint proc JS_IsJobPending*(rt: JSRuntime): JS_BOOL proc JS_ExecutePendingJob*(rt: JSRuntime; pctx: ptr JSContext): cint diff --git a/src/config/config.nim b/src/config/config.nim index 56c448d7..b38d3129 100644 --- a/src/config/config.nim +++ b/src/config/config.nim @@ -131,6 +131,7 @@ type Config* = ref object jsctx: JSContext + jsvfns*: seq[JSValueFunction] configdir {.jsget.}: string `include` {.jsget.}: seq[ChaPathResolved] start* {.jsget.}: StartConfig @@ -363,7 +364,7 @@ proc parseConfigValue(ctx: var ConfigParser; x: var object; v: TomlValue; k: string) = typeCheck(v, tvtTable, k) for fk, fv in x.fieldPairs: - when typeof(fv) isnot JSContext: + when typeof(fv) isnot JSContext|seq[JSValueFunction]: let kebabk = snakeToKebabCase(fk) if kebabk in v: let kkk = if k != "": @@ -597,6 +598,7 @@ proc parseConfigValue(ctx: var ConfigParser; x: var JSValueFunction; if not JS_IsFunction(ctx.config.jsctx, fun): raise newException(ValueError, k & " is not a function") x = JSValueFunction(fun: fun) + ctx.config.jsvfns.add(x) # so we can clean it up on exit proc parseConfigValue(ctx: var ConfigParser; x: var ChaPathResolved; v: TomlValue; k: string) = @@ -667,8 +669,7 @@ proc parseConfigValue(ctx: var ConfigParser; x: var CommandConfig; v: TomlValue; if vv.t == tvtTable: ctx.parseConfigValue(x, vv, kkk) else: # tvtString - # skip initial "cmd.", we don't need it - x.init.add((kkk.substr("cmd.".len), vv.s)) + x.init.add((kkk, vv.s)) type ParseConfigResult* = object success*: bool @@ -797,9 +798,11 @@ proc initCommands*(config: Config): Err[string] = if JS_IsException(fun): return err(ctx.getExceptionMsg()) if not JS_IsFunction(ctx, fun): + JS_FreeValue(ctx, fun) return err(k & " is not a function") ctx.definePropertyE(objIt, name, JS_DupValue(ctx, fun)) config.cmd.map[k] = fun + JS_FreeValue(ctx, objIt) config.cmd.jsObj = JS_DupValue(ctx, obj) config.cmd.init = @[] ok() diff --git a/src/config/toml.nim b/src/config/toml.nim index f8ab9a08..992a0cbc 100644 --- a/src/config/toml.nim +++ b/src/config/toml.nim @@ -369,12 +369,11 @@ proc consumeNoState(state: var TomlParser): Result[bool, TomlError] = let key = table.key.join('.') return state.err("re-definition of node " & key & " as table array (was " & $last.t & ")") - last.ad = true let val = TomlValue(t: tvtTable, tab: table) last.a.add(val) else: let val = TomlValue(t: tvtTable, tab: table) - let last = TomlValue(t: tvtArray, a: @[val]) + let last = TomlValue(t: tvtArray, a: @[val], ad: true) node.map[table.key[^1]] = last state.currkey = table.key state.node = table diff --git a/src/html/dom.nim b/src/html/dom.nim index 6f57960a..739d01fe 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -25,6 +25,7 @@ import js/domexception import js/error import js/fromjs import js/javascript +import js/jsutils import js/opaque import js/propertyenumlist import js/timeout @@ -370,7 +371,6 @@ jsDestructor(Navigator) jsDestructor(PluginArray) jsDestructor(MimeTypeArray) jsDestructor(Screen) -jsDestructor(Window) jsDestructor(Element) jsDestructor(HTMLElement) @@ -1276,21 +1276,22 @@ const SupportedTokensMap = { "next", "pingback", "preconnect", "prefetch", "preload", "search", "stylesheet" ] -}.toTable() +} func supports(tokenList: DOMTokenList; token: string): JSResult[bool] {.jsfunc.} = let localName = tokenList.element.document.toStaticAtom(tokenList.localName) - if localName in SupportedTokensMap: - let lowercase = token.toLowerAscii() - return ok(lowercase in SupportedTokensMap[localName]) + for it in SupportedTokensMap: + if it[0] == localName: + let lowercase = token.toLowerAscii() + return ok(lowercase in it[1]) return err(newTypeError("No supported tokens defined for attribute")) func value(tokenList: DOMTokenList): string {.jsfget.} = return $tokenList -func getter(tokenList: DOMTokenList; i: int): Option[string] {.jsgetprop.} = - return tokenList.item(i) +func getter(tokenList: DOMTokenList; i: uint32): Option[string] {.jsgetprop.} = + return tokenList.item(int(i)) # DOMStringMap func validateAttributeName(name: string): Err[DOMException] = @@ -1352,15 +1353,15 @@ func names(ctx: JSContext; map: ptr DOMStringMap): JSPropertyEnumList func length(nodeList: NodeList): uint32 {.jsfget.} = return uint32(nodeList.len) -func hasprop(nodeList: NodeList; i: int): bool {.jshasprop.} = - return i < nodeList.len +func hasprop(nodeList: NodeList; i: uint32): bool {.jshasprop.} = + return int(i) < nodeList.len func item(nodeList: NodeList; i: int): Node {.jsfunc.} = if i < nodeList.len: return nodeList.snapshot[i] -func getter(nodeList: NodeList; i: int): Option[Node] {.jsgetprop.} = - return option(nodeList.item(i)) +func getter(nodeList: NodeList; i: uint32): Option[Node] {.jsgetprop.} = + return option(nodeList.item(int(i))) func names(ctx: JSContext; nodeList: NodeList): JSPropertyEnumList {.jspropnames.} = @@ -1417,14 +1418,15 @@ func names(ctx: JSContext; collection: HTMLCollection): JSPropertyEnumList proc length(collection: HTMLAllCollection): uint32 {.jsfget.} = return uint32(collection.len) -func hasprop(collection: HTMLAllCollection; i: int): bool {.jshasprop.} = - return i < collection.len +func hasprop(collection: HTMLAllCollection; i: uint32): bool {.jshasprop.} = + return int(i) < collection.len -func item(collection: HTMLAllCollection; i: int): Element {.jsfunc.} = +func item(collection: HTMLAllCollection; i: uint32): Element {.jsfunc.} = + let i = int(i) if i < collection.len: return Element(collection.snapshot[i]) -func getter(collection: HTMLAllCollection; i: int): Option[Element] +func getter(collection: HTMLAllCollection; i: uint32): Option[Element] {.jsgetprop.} = return option(collection.item(i)) @@ -1452,7 +1454,7 @@ proc newLocation*(window: Window): Location = let ctx = window.jsctx if ctx != nil: let val = toJS(ctx, location) - let valueOf = ctx.getOpaque().Object_prototype_valueOf + let valueOf = ctx.getOpaque().valRefs[jsvObjectPrototypeValueOf] defineProperty(ctx, val, "valueOf", JS_DupValue(ctx, valueOf)) defineProperty(ctx, val, "toPrimitive", JS_UNDEFINED) #TODO [[DefaultProperties]] @@ -4179,7 +4181,8 @@ proc toBlob(ctx: JSContext; this: HTMLCanvasElement; callback: JSValue; let buf = this.bitmap.toPNG(outlen) let blob = newBlob(buf, outlen, "image/png", proc() = dealloc(buf)) var jsBlob = toJS(ctx, blob) - let res = JS_Call(ctx, callback, JS_UNDEFINED, 1, addr jsBlob) + let res = JS_Call(ctx, callback, JS_UNDEFINED, 1, jsBlob.toJSValueArray()) + JS_FreeValue(ctx, jsBlob) # Hack. TODO: implement JSValue to callback if res == JS_EXCEPTION: return JS_EXCEPTION diff --git a/src/html/env.nim b/src/html/env.nim index c9d99dc2..00b6f18c 100644 --- a/src/html/env.nim +++ b/src/html/env.nim @@ -68,10 +68,10 @@ proc item(pluginArray: ptr PluginArray): JSValue {.jsfunc.} = JS_NULL proc length(pluginArray: ptr PluginArray): uint32 {.jsfget.} = 0 proc item(mimeTypeArray: ptr MimeTypeArray): JSValue {.jsfunc.} = JS_NULL proc length(mimeTypeArray: ptr MimeTypeArray): uint32 {.jsfget.} = 0 -proc getter(pluginArray: ptr PluginArray; i: int): Option[JSValue] +proc getter(pluginArray: ptr PluginArray; i: uint32): Option[JSValue] {.jsgetprop.} = discard -proc getter(mimeTypeArray: ptr MimeTypeArray; i: int): Option[JSValue] +proc getter(mimeTypeArray: ptr MimeTypeArray; i: uint32): Option[JSValue] {.jsgetprop.} = discard @@ -193,7 +193,7 @@ proc addScripting*(window: Window; selector: Selector[int]) = ctx.addEventModule() let eventTargetCID = ctx.getClass("EventTarget") ctx.registerType(Window, asglobal = true, parent = eventTargetCID) - ctx.setGlobal(global, window) + ctx.setGlobal(window) JS_FreeValue(ctx, global) ctx.addDOMExceptionModule() ctx.addConsoleModule() diff --git a/src/html/event.nim b/src/html/event.nim index 9616d55e..b74649e8 100644 --- a/src/html/event.nim +++ b/src/html/event.nim @@ -7,6 +7,7 @@ import js/error import js/fromjs import js/javascript import js/jstypes +import js/jsutils import js/tojs import types/opt @@ -198,9 +199,10 @@ proc invoke*(ctx: JSContext; listener: EventListener; event: Event): if JS_IsNull(listener.callback): return JS_UNDEFINED let jsTarget = ctx.toJS(event.currentTarget) - var jsEvent = ctx.toJS(event) + let jsEvent = ctx.toJS(event) if JS_IsFunction(ctx, listener.callback): - let ret = JS_Call(ctx, listener.callback, jsTarget, 1, addr jsEvent) + let ret = JS_Call(ctx, listener.callback, jsTarget, 1, + jsEvent.toJSValueArray()) JS_FreeValue(ctx, jsTarget) JS_FreeValue(ctx, jsEvent) return ret @@ -210,7 +212,7 @@ proc invoke*(ctx: JSContext; listener: EventListener; event: Event): JS_FreeValue(ctx, jsTarget) JS_FreeValue(ctx, jsEvent) return handler - let ret = JS_Call(ctx, handler, jsTarget, 1, addr jsEvent) + let ret = JS_Call(ctx, handler, jsTarget, 1, jsEvent.toJSValueArray()) JS_FreeValue(ctx, jsTarget) JS_FreeValue(ctx, jsEvent) return ret @@ -255,9 +257,12 @@ proc flattenMore(ctx: JSContext; options: JSValue): var once = false var passive: Option[bool] if JS_IsObject(options): - once = fromJS[bool](ctx, JS_GetPropertyStr(ctx, options, "once")) - .get(false) - let x = fromJS[bool](ctx, JS_GetPropertyStr(ctx, options, "passive")) + let jsOnce = JS_GetPropertyStr(ctx, options, "once") + once = fromJS[bool](ctx, jsOnce).get(false) + JS_FreeValue(ctx, jsOnce) + let jsPassive = JS_GetPropertyStr(ctx, options, "passive") + let x = fromJS[bool](ctx, jsPassive) + JS_FreeValue(ctx, jsPassive) if x.isSome: passive = some(x.get) return (capture, once, passive) diff --git a/src/io/dynstream.nim b/src/io/dynstream.nim index 8032f5ec..65077734 100644 --- a/src/io/dynstream.nim +++ b/src/io/dynstream.nim @@ -45,10 +45,12 @@ proc sendDataLoop*(s: DynStream; buffer: pointer; len: int) = break proc sendDataLoop*(s: DynStream; buffer: openArray[uint8]) {.inline.} = - s.sendDataLoop(unsafeAddr buffer[0], buffer.len) + if buffer.len > 0: + s.sendDataLoop(unsafeAddr buffer[0], buffer.len) proc sendDataLoop*(s: DynStream; buffer: openArray[char]) {.inline.} = - s.sendDataLoop(unsafeAddr buffer[0], buffer.len) + if buffer.len > 0: + s.sendDataLoop(unsafeAddr buffer[0], buffer.len) proc write*(s: DynStream; buffer: openArray[char]) {.inline.} = s.sendDataLoop(buffer) diff --git a/src/js/console.nim b/src/js/console.nim index 305dfa9b..3b28df5e 100644 --- a/src/js/console.nim +++ b/src/js/console.nim @@ -19,8 +19,8 @@ proc newConsole*(err: DynStream; clearFun: proc() = nil; showFun: proc() = nil; ) proc log*(console: Console; ss: varargs[string]) {.jsfunc.} = - for i in 0..<ss.len: - console.err.write(ss[i]) + for i, s in ss: + console.err.write(s) if i != ss.high: console.err.write(' ') console.err.write('\n') diff --git a/src/js/domexception.nim b/src/js/domexception.nim index 3d4a4a1e..f916fb59 100644 --- a/src/js/domexception.nim +++ b/src/js/domexception.nim @@ -37,7 +37,7 @@ type jsDestructor(DOMException) -proc newDOMException*(message = "", name = "Error"): DOMException {.jsctor.} = +proc newDOMException*(message = ""; name = "Error"): DOMException {.jsctor.} = return DOMException( e: JS_DOM_EXCEPTION, name: name, diff --git a/src/js/fromjs.nim b/src/js/fromjs.nim index 527b6221..c5c80da1 100644 --- a/src/js/fromjs.nim +++ b/src/js/fromjs.nim @@ -7,6 +7,7 @@ import bindings/quickjs import io/promise import js/error import js/jstypes +import js/jsutils import js/opaque import types/opt import utils/twtstr @@ -84,11 +85,6 @@ func fromJSInt[T: SomeInteger](ctx: JSContext; val: JSValue): if JS_ToInt32(ctx, addr ret, val) < 0: return err() return ok(int(ret)) - elif T is uint: - var ret: uint32 - if JS_ToUint32(ctx, addr ret, val) < 0: - return err() - return ok(uint(ret)) elif T is int32: var ret: int32 if JS_ToInt32(ctx, addr ret, val) < 0: @@ -123,11 +119,11 @@ macro fromJSTupleBody(a: tuple) = var `done`: bool) for i in 0..<len: result.add(quote do: - let next = JS_Call(ctx, next_method, it, 0, nil) + let next = JS_Call(ctx, nextMethod, it, 0, nil) if JS_IsException(next): return err() defer: JS_FreeValue(ctx, next) - let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE]) + let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().strRefs[jstDone]) if JS_IsException(doneVal): return err() defer: JS_FreeValue(ctx, doneVal) @@ -135,7 +131,7 @@ macro fromJSTupleBody(a: tuple) = if `done`: return errTypeError("Too few arguments in sequence (got " & $`i` & ", expected " & $`len` & ")") - let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[VALUE]) + let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().strRefs[jstValue]) if JS_IsException(valueVal): return err() defer: JS_FreeValue(ctx, valueVal) @@ -143,23 +139,23 @@ macro fromJSTupleBody(a: tuple) = ) if i == len - 1: result.add(quote do: - let next = JS_Call(ctx, next_method, it, 0, nil) + let next = JS_Call(ctx, nextMethod, it, 0, nil) if JS_IsException(next): return err() defer: JS_FreeValue(ctx, next) - let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE]) + let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().strRefs[jstDone]) `done` = ?fromJS[bool](ctx, doneVal) var i = `i` # we're emulating a sequence, so we must query all remaining parameters # too: while not `done`: inc i - let next = JS_Call(ctx, next_method, it, 0, nil) + let next = JS_Call(ctx, nextMethod, it, 0, nil) if JS_IsException(next): return err() defer: JS_FreeValue(ctx, next) let doneVal = JS_GetProperty(ctx, next, - ctx.getOpaque().str_refs[DONE]) + ctx.getOpaque().strRefs[jstDone]) if JS_IsException(doneVal): return err() defer: JS_FreeValue(ctx, doneVal) @@ -169,11 +165,11 @@ macro fromJSTupleBody(a: tuple) = ", expected " & $`len` & ")" return err(newTypeError(msg)) JS_FreeValue(ctx, JS_GetProperty(ctx, next, - ctx.getOpaque().str_refs[VALUE])) + ctx.getOpaque().strRefs[jstValue])) ) proc fromJSTuple[T: tuple](ctx: JSContext; val: JSValue): JSResult[T] = - let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_refs[ITERATOR]) + let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().symRefs[jsyIterator]) if JS_IsException(itprop): return err() defer: JS_FreeValue(ctx, itprop) @@ -181,16 +177,16 @@ proc fromJSTuple[T: tuple](ctx: JSContext; val: JSValue): JSResult[T] = if JS_IsException(it): return err() defer: JS_FreeValue(ctx, it) - let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().str_refs[NEXT]) - if JS_IsException(next_method): + let nextMethod = JS_GetProperty(ctx, it, ctx.getOpaque().strRefs[jstNext]) + if JS_IsException(nextMethod): return err() - defer: JS_FreeValue(ctx, next_method) + defer: JS_FreeValue(ctx, nextMethod) var x: T fromJSTupleBody(x) return ok(x) proc fromJSSeq[T](ctx: JSContext; val: JSValue): JSResult[seq[T]] = - let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_refs[ITERATOR]) + let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().symRefs[jsyIterator]) if JS_IsException(itprop): return err() defer: JS_FreeValue(ctx, itprop) @@ -198,24 +194,24 @@ proc fromJSSeq[T](ctx: JSContext; val: JSValue): JSResult[seq[T]] = if JS_IsException(it): return err() defer: JS_FreeValue(ctx, it) - let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().str_refs[NEXT]) - if JS_IsException(next_method): + let nextMethod = JS_GetProperty(ctx, it, ctx.getOpaque().strRefs[jstNext]) + if JS_IsException(nextMethod): return err() - defer: JS_FreeValue(ctx, next_method) + defer: JS_FreeValue(ctx, nextMethod) var s = newSeq[T]() while true: - let next = JS_Call(ctx, next_method, it, 0, nil) + let next = JS_Call(ctx, nextMethod, it, 0, nil) if JS_IsException(next): return err() defer: JS_FreeValue(ctx, next) - let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE]) + let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().strRefs[jstDone]) if JS_IsException(doneVal): return err() defer: JS_FreeValue(ctx, doneVal) let done = ?fromJS[bool](ctx, doneVal) if done: break - let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[VALUE]) + let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().strRefs[jstValue]) if JS_IsException(valueVal): return err() defer: JS_FreeValue(ctx, valueVal) @@ -224,7 +220,7 @@ proc fromJSSeq[T](ctx: JSContext; val: JSValue): JSResult[seq[T]] = return ok(s) proc fromJSSet[T](ctx: JSContext; val: JSValue): JSResult[set[T]] = - let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().sym_refs[ITERATOR]) + let itprop = JS_GetProperty(ctx, val, ctx.getOpaque().symRefs[jsyIterator]) if JS_IsException(itprop): return err() defer: JS_FreeValue(ctx, itprop) @@ -232,28 +228,28 @@ proc fromJSSet[T](ctx: JSContext; val: JSValue): JSResult[set[T]] = if JS_IsException(it): return err() defer: JS_FreeValue(ctx, it) - let next_method = JS_GetProperty(ctx, it, ctx.getOpaque().str_refs[NEXT]) - if JS_IsException(next_method): + let nextMethod = JS_GetProperty(ctx, it, ctx.getOpaque().strRefs[jstNext]) + if JS_IsException(nextMethod): return err() - defer: JS_FreeValue(ctx, next_method) + defer: JS_FreeValue(ctx, nextMethod) var s: set[T] while true: - let next = JS_Call(ctx, next_method, it, 0, nil) + let next = JS_Call(ctx, nextMethod, it, 0, nil) if JS_IsException(next): return err() defer: JS_FreeValue(ctx, next) - let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[DONE]) + let doneVal = JS_GetProperty(ctx, next, ctx.getOpaque().strRefs[jstDone]) if JS_IsException(doneVal): return err() defer: JS_FreeValue(ctx, doneVal) let done = ?fromJS[bool](ctx, doneVal) if done: break - let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().str_refs[VALUE]) + let valueVal = JS_GetProperty(ctx, next, ctx.getOpaque().strRefs[jstValue]) if JS_IsException(valueVal): return err() defer: JS_FreeValue(ctx, valueVal) - let genericRes = ?fromJS[typeof(s.items)](ctx, valueVal) + let genericRes = ?fromJS[T](ctx, valueVal) s.incl(genericRes) return ok(s) @@ -341,12 +337,14 @@ proc fromJSDict[T: JSDict](ctx: JSContext; val: JSValue): JSResult[T] = if not JS_IsUndefined(val) and not JS_IsNull(val) and not JS_IsObject(val): return err(newTypeError("Dictionary is not an object")) #TODO throw on missing required values - var d: T + var d = T() if JS_IsObject(val): for k, v in d.fieldPairs: let esm = JS_GetPropertyStr(ctx, val, k) if not JS_IsUndefined(esm): v = ?fromJS[typeof(v)](ctx, esm) + when v isnot JSValue: + JS_FreeValue(ctx, esm) return ok(d) proc fromJSArrayBuffer(ctx: JSContext; val: JSValue): JSResult[JSArrayBuffer] = @@ -377,25 +375,34 @@ proc fromJSArrayBufferView(ctx: JSContext; val: JSValue): return ok(view) proc promiseThenCallback(ctx: JSContext; this_val: JSValue; argc: cint; - argv: ptr JSValue; magic: cint; func_data: ptr JSValue): JSValue {.cdecl.} = - let op = JS_GetOpaque(func_data[], JS_GetClassID(func_data[])) - let p = cast[EmptyPromise](op) - p.resolve() - GC_unref(p) + argv: ptr UncheckedArray[JSValue]; magic: cint; + func_data: ptr UncheckedArray[JSValue]): JSValue {.cdecl.} = + let fun = func_data[0] + let op = JS_GetOpaque(fun, JS_GetClassID(fun)) + if op != nil: + let p = cast[EmptyPromise](op) + p.resolve() + GC_unref(p) + JS_SetOpaque(fun, nil) return JS_UNDEFINED proc fromJSEmptyPromise(ctx: JSContext; val: JSValue): JSResult[EmptyPromise] = if not JS_IsObject(val): return err(newTypeError("Value is not an object")) - #TODO I have a feeling this leaks memory in some cases :( var p = EmptyPromise() GC_ref(p) - var tmp = JS_NewObject(ctx) + let tmp = JS_NewObject(ctx) JS_SetOpaque(tmp, cast[pointer](p)) - var fun = JS_NewCFunctionData(ctx, promiseThenCallback, 0, 0, 1, addr tmp) - let res = JS_Invoke(ctx, val, ctx.getOpaque().str_refs[THEN], 1, addr fun) + let fun = JS_NewCFunctionData(ctx, promiseThenCallback, 0, 0, 1, + tmp.toJSValueArray()) + JS_FreeValue(ctx, tmp) + let res = JS_Invoke(ctx, val, ctx.getOpaque().strRefs[jstThen], 1, + fun.toJSValueArray()) + JS_FreeValue(ctx, fun) if JS_IsException(res): + JS_FreeValue(ctx, res) return err() + JS_FreeValue(ctx, res) return ok(p) type FromJSAllowedT = (object and not (Result|Option|Table|JSValue|JSDict| @@ -445,18 +452,25 @@ proc fromJS*[T](ctx: JSContext; val: JSValue): JSResult[T] = else: return fromJS2(ctx, val, $T) -const JS_ATOM_TAG_INT = cuint(1u32 shl 31) +const JS_ATOM_TAG_INT = 1u32 shl 31 func JS_IsNumber*(v: JSAtom): JS_BOOL = - return (cast[cuint](v) and JS_ATOM_TAG_INT) != 0 + return (uint32(v) and JS_ATOM_TAG_INT) != 0 -func fromJS*[T: string|uint32](ctx: JSContext; atom: JSAtom): Opt[T] = - when T is SomeNumber: +func fromJS*[T: string|uint32|JSAtom](ctx: JSContext; atom: JSAtom): Opt[T] = + when T is JSAtom: + return ok(atom) + elif T is SomeNumber: if JS_IsNumber(atom): - return ok(T(cast[uint32](atom) and (not JS_ATOM_TAG_INT))) + return ok(uint32(atom) and (not JS_ATOM_TAG_INT)) + return err() else: - let val = JS_AtomToValue(ctx, atom) - return toString(ctx, val) + let cs = JS_AtomToCString(ctx, atom) + if cs == nil: + return err() + let s = $cs + JS_FreeCString(ctx, cs) + return ok(s) proc fromJSPObj[T](ctx: JSContext; val: JSValue): JSResult[ptr T] = return cast[JSResult[ptr T]](fromJSPObj0(ctx, val, $T)) diff --git a/src/js/javascript.nim b/src/js/javascript.nim index 8a1a0794..2266a3c7 100644 --- a/src/js/javascript.nim +++ b/src/js/javascript.nim @@ -30,8 +30,8 @@ # 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 must be either a string or an integer (preferably uint32), -# since it is converted from a JSAtom. +# nothing. The key must be either a JSAtom, uint32 or string. (Note that the +# string option copies.) # {.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. @@ -77,11 +77,11 @@ export JS_EVAL_FLAG_STRIP, JS_EVAL_FLAG_COMPILE_ONLY -export JSRuntime, JSContext, JSValue, JSClassID +export JSRuntime, JSContext, JSValue, JSClassID, JSAtom export JS_GetGlobalObject, JS_FreeValue, JS_IsException, JS_GetPropertyStr, - JS_IsFunction, JS_NewCFunctionData, JS_Call, JS_DupValue + JS_IsFunction, JS_NewCFunctionData, JS_Call, JS_DupValue, JS_IsUndefined when sizeof(int) < sizeof(int64): export quickjs.`==` @@ -159,40 +159,41 @@ func newJSCFunction*(ctx: JSContext; name: string; fun: JSCFunction; return JS_NewCFunction2(ctx, fun, cstring(name), cint(argc), proto, cint(magic)) -proc free*(ctx: var JSContext) = +proc free*(ctx: JSContext) = var opaque = ctx.getOpaque() if opaque != nil: - for a in opaque.sym_refs: + for a in opaque.symRefs: JS_FreeAtom(ctx, a) - for a in opaque.str_refs: + 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) - JS_FreeValue(ctx, opaque.Array_prototype_values) - JS_FreeValue(ctx, opaque.Object_prototype_valueOf) - JS_FreeValue(ctx, opaque.Uint8Array_ctor) - JS_FreeValue(ctx, opaque.Set_ctor) - JS_FreeValue(ctx, opaque.Function_ctor) - for v in opaque.err_ctors: + for v in opaque.errCtorRefs: JS_FreeValue(ctx, v) + if opaque.globalUnref != nil: + opaque.globalUnref() GC_unref(opaque) JS_FreeContext(ctx) - ctx = nil -proc free*(rt: var JSRuntime) = +proc free*(rt: JSRuntime) = let opaque = rt.getOpaque() GC_unref(opaque) JS_FreeRuntime(rt) runtimes.del(runtimes.find(rt)) - rt = nil -proc setGlobal*[T](ctx: JSContext; global: JSValue; obj: T) = +proc setGlobal*[T](ctx: JSContext; obj: T) = # Add JSValue reference. - let p = JS_VALUE_GET_PTR(global) - let header = cast[ptr JSRefCountHeader](p) - inc header.ref_count - ctx.setOpaque(global, cast[pointer](obj)) + let global = JS_GetGlobalObject(ctx) + let opaque = cast[pointer](obj) + ctx.setOpaque(global, opaque) GC_ref(obj) + let rtOpaque = JS_GetRuntime(ctx).getOpaque() + ctx.getOpaque().globalUnref = proc() = + GC_unref(obj) + rtOpaque.plist.del(opaque) + JS_FreeValue(ctx, global) proc setInterruptHandler*(rt: JSRuntime; cb: JSInterruptHandler; opaque: pointer = nil) = @@ -266,13 +267,13 @@ func newJSClass*(ctx: JSContext; cdef: JSClassDefConst; tname: string; ctxOpaque.htmldda = result if finalizer != nil: rtOpaque.fins[result] = finalizer - var proto: JSValue - if parent != 0: + let proto = if parent != 0: let parentProto = JS_GetClassProto(ctx, parent) - proto = JS_NewObjectProtoClass(ctx, parentProto, parent) + let x = JS_NewObjectProtoClass(ctx, parentProto, parent) JS_FreeValue(ctx, parentProto) + x else: - proto = JS_NewObject(ctx) + JS_NewObject(ctx) if funcs.len > 0: # We avoid funcs being GC'ed by putting the list in rtOpaque. # (QuickJS uses the pointer later.) @@ -282,11 +283,12 @@ func newJSClass*(ctx: JSContext; cdef: JSClassDefConst; tname: string; 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) - doAssert JS_SetProperty(ctx, proto, ctxOpaque.sym_refs[ITERATOR], val) == 1 + let val = JS_DupValue(ctx, ctxOpaque.valRefs[jsvArrayPrototypeValues]) + let itSym = ctxOpaque.symRefs[jsyIterator] + doAssert JS_SetProperty(ctx, proto, itSym, val) == 1 let news = JS_NewAtomString(ctx, cdef.class_name) doAssert not JS_IsException(news) - ctx.definePropertyC(proto, ctxOpaque.sym_refs[TO_STRING_TAG], + ctx.definePropertyC(proto, ctxOpaque.symRefs[jsyToStringTag], JS_DupValue(ctx, news)) JS_SetClassProto(ctx, result, proto) ctx.addClassUnforgeable(proto, result, parent, unforgeable) @@ -295,7 +297,7 @@ func newJSClass*(ctx: JSContext; cdef: JSClassDefConst; tname: string; assert ctxOpaque.gclaz == "" ctxOpaque.gclaz = tname ctxOpaque.gparent = parent - ctx.definePropertyC(global, ctxOpaque.sym_refs[TO_STRING_TAG], + 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: " & @@ -313,7 +315,7 @@ func newJSClass*(ctx: JSContext; cdef: JSClassDefConst; tname: string; cint(staticfuns.len)) JS_SetConstructor(ctx, jctor, proto) if errid.isSome: - ctx.getOpaque().err_ctors[errid.get] = JS_DupValue(ctx, jctor) + ctx.getOpaque().errCtorRefs[errid.get] = JS_DupValue(ctx, jctor) ctxOpaque.ctors[result] = JS_DupValue(ctx, jctor) if not nointerface: if JS_IsNull(namespace): @@ -322,6 +324,8 @@ func newJSClass*(ctx: JSContext; cdef: JSClassDefConst; tname: string; JS_FreeValue(ctx, global) else: ctx.definePropertyCW(namespace, $cdef.class_name, jctor) + else: + JS_FreeValue(ctx, jctor) type FuncParam = tuple name: string @@ -596,7 +600,7 @@ proc addUnionParamBranch(gen: var JSFuncGenerator; query, newBranch: NimNode; func isSequence*(ctx: JSContext; o: JSValue): bool = if not JS_IsObject(o): return false - let prop = JS_GetProperty(ctx, o, ctx.getOpaque().sym_refs[ITERATOR]) + let prop = JS_GetProperty(ctx, o, ctx.getOpaque().symRefs[jsyIterator]) # prop can't be exception (throws_ref_error is 0 and tag is object) result = not JS_IsUndefined(prop) JS_FreeValue(ctx, prop) @@ -1358,10 +1362,9 @@ func jsname(info: RegistryInfo): string = return info.tname proc newRegistryInfo(t: NimNode; name: string): RegistryInfo = - let info = RegistryInfo( + return RegistryInfo( t: t, name: name, - dfin: ident("js_" & t.strVal & "ClassCheckDestroy"), classDef: ident("classDef"), tabList: newNimNode(nnkBracket), tabUnforgeable: newNimNode(nnkBracket), @@ -1374,9 +1377,6 @@ proc newRegistryInfo(t: NimNode; name: string): RegistryInfo = propHasFun: newNilLit(), propNamesFun: newNilLit() ) - if info.tname notin jsDtors: - warning("No destructor has been defined for type " & info.tname) - return info proc bindConstructor(stmts: NimNode; info: var RegistryInfo): NimNode = if info.ctorFun != nil: @@ -1604,14 +1604,21 @@ proc bindEndStmts(endstmts: NimNode; info: RegistryInfo) = ) let `classDef` = JSClassDefConst(addr cd)) -macro registerType*(ctx: typed; t: typed; parent: JSClassID = 0, - asglobal = false, nointerface = false, name: static string = "", - has_extra_getset: static bool = false, - extra_getset: static openArray[TabGetSet] = [], - namespace: JSValue = JS_NULL, errid = opt(JSErrorEnum), - ishtmldda = false): JSClassID = +macro registerType*(ctx: typed; t: typed; parent: JSClassID = 0; + asglobal: static bool = false; nointerface = false; name: static string = ""; + has_extra_getset: static bool = false; + extra_getset: static openArray[TabGetSet] = []; namespace = JS_NULL; + errid = opt(JSErrorEnum); ishtmldda = false): JSClassID = var stmts = newStmtList() var info = newRegistryInfo(t, name) + if not asglobal: + info.dfin = ident("js_" & t.strVal & "ClassCheckDestroy") + 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) @@ -1623,7 +1630,8 @@ macro registerType*(ctx: typed; t: typed; parent: JSClassID = 0, # been passed to it at all. stmts.bindExtraGetSet(info, extra_getset) let sctr = stmts.bindConstructor(info) - stmts.bindCheckDestroy(info) + if not asglobal: + stmts.bindCheckDestroy(info) let endstmts = newStmtList() endstmts.bindEndStmts(info) let tabList = info.tabList diff --git a/src/js/jsutils.nim b/src/js/jsutils.nim new file mode 100644 index 00000000..b8a8398e --- /dev/null +++ b/src/js/jsutils.nim @@ -0,0 +1,9 @@ +import bindings/quickjs + +template toJSValueArray*(a: openArray[JSValue]): ptr UncheckedArray[JSValue] = + cast[ptr UncheckedArray[JSValue]](unsafeAddr a[0]) + +# Warning: this must be a template, because we're taking the address of +# the passed value, and Nim is pass-by-value. +template toJSValueArray*(a: JSValue): ptr UncheckedArray[JSValue] = + cast[ptr UncheckedArray[JSValue]](unsafeAddr a) diff --git a/src/js/opaque.nim b/src/js/opaque.nim index 490747bc..93900502 100644 --- a/src/js/opaque.nim +++ b/src/js/opaque.nim @@ -5,17 +5,24 @@ import js/error import types/opt type - JSSymbolRefs* = enum - ITERATOR = "iterator" - ASYNC_ITERATOR = "asyncIterator" - TO_STRING_TAG = "toStringTag" - - JSStrRefs* = enum - DONE = "done" - VALUE = "value" - NEXT = "next" - PROTOTYPE = "prototype" - THEN = "then" + JSSymbolRef* = enum + jsyIterator = "iterator" + jsyAsyncIterator = "asyncIterator" + jsyToStringTag = "toStringTag" + + JSStrRef* = enum + jstDone = "done" + jstValue = "value" + jstNext = "next" + jstPrototype = "prototype" + jstThen = "then" + + JSValueRef* = enum + jsvArrayPrototypeValues = "Array.prototype.values" + jsvUint8Array = "Uint8Array" + jsvObjectPrototypeValueOf = "Object.prototype.valueOf" + jsvSet = "Set" + jsvFunction = "Function" JSContextOpaque* = ref object creg*: Table[string, JSClassID] @@ -28,15 +35,12 @@ type unforgeable*: Table[JSClassID, seq[JSCFunctionListEntry]] gclaz*: string gparent*: JSClassID - sym_refs*: array[JSSymbolRefs, JSAtom] - str_refs*: array[JSStrRefs, JSAtom] - Array_prototype_values*: JSValue - Object_prototype_valueOf*: JSValue - Uint8Array_ctor*: JSValue - Set_ctor*: JSValue - Function_ctor*: JSValue - err_ctors*: array[JSErrorEnum, JSValue] + symRefs*: array[JSSymbolRef, JSAtom] + strRefs*: array[JSStrRef, JSAtom] + valRefs*: array[JSValueRef, JSValue] + errCtorRefs*: array[JSErrorEnum, JSValue] htmldda*: JSClassID # only one of these exists: document.all. + globalUnref*: proc() {.closure.} JSFinalizerFunction* = proc(rt: JSRuntime; val: JSValue) {.nimcall.} @@ -51,41 +55,26 @@ func newJSContextOpaque*(ctx: JSContext): JSContextOpaque = let opaque = JSContextOpaque() block: # get well-known symbols and other functions let global = JS_GetGlobalObject(ctx) - block: - let sym = JS_GetPropertyStr(ctx, global, "Symbol") - for s in JSSymbolRefs: - let name = $s - let val = JS_GetPropertyStr(ctx, sym, cstring(name)) - assert JS_IsSymbol(val) - opaque.sym_refs[s] = JS_ValueToAtom(ctx, val) - JS_FreeValue(ctx, val) - JS_FreeValue(ctx, sym) - for s in JSStrRefs: - let ss = $s - opaque.str_refs[s] = JS_NewAtomLen(ctx, cstring(ss), csize_t(ss.len)) - block: - let arrproto = JS_GetClassProto(ctx, JS_CLASS_ARRAY) - opaque.Array_prototype_values = JS_GetPropertyStr(ctx, arrproto, - "values") - JS_FreeValue(ctx, arrproto) - block: - let objproto = JS_GetClassProto(ctx, JS_CLASS_OBJECT) - opaque.Object_prototype_valueOf = JS_GetPropertyStr(ctx, objproto, - "valueOf") - JS_FreeValue(ctx, objproto) - block: - opaque.Uint8Array_ctor = JS_GetPropertyStr(ctx, global, "Uint8Array") - assert not JS_IsException(opaque.Uint8Array_ctor) - block: - opaque.Set_ctor = JS_GetPropertyStr(ctx, global, "Set") - assert not JS_IsException(opaque.Set_ctor) - block: - opaque.Function_ctor = JS_GetPropertyStr(ctx, global, "Function") - assert not JS_IsException(opaque.Function_ctor) + let sym = JS_GetPropertyStr(ctx, global, "Symbol") + for s in JSSymbolRef: + let name = $s + let val = JS_GetPropertyStr(ctx, sym, cstring(name)) + assert JS_IsSymbol(val) + opaque.symRefs[s] = JS_ValueToAtom(ctx, val) + JS_FreeValue(ctx, val) + JS_FreeValue(ctx, sym) + for s in JSStrRef: + let ss = $s + opaque.strRefs[s] = JS_NewAtomLen(ctx, cstring(ss), csize_t(ss.len)) + for s in JSValueRef: + let ss = $s + let ret = JS_Eval(ctx, cstring(ss), csize_t(ss.len), "<init>", 0) + assert JS_IsFunction(ctx, ret) + opaque.valRefs[s] = ret for e in JSErrorEnum: let s = $e let err = JS_GetPropertyStr(ctx, global, cstring(s)) - opaque.err_ctors[e] = err + opaque.errCtorRefs[e] = err JS_FreeValue(ctx, global) return opaque diff --git a/src/js/tojs.nim b/src/js/tojs.nim index 2ee8fd47..7831a6a9 100644 --- a/src/js/tojs.nim +++ b/src/js/tojs.nim @@ -44,6 +44,7 @@ import bindings/quickjs import io/promise import js/error import js/jstypes +import js/jsutils import js/opaque import js/typeptr import types/opt @@ -148,8 +149,8 @@ proc newFunction*(ctx: JSContext; args: openArray[string]; body: string): for arg in args: paramList.add(toJS(ctx, arg)) paramList.add(toJS(ctx, body)) - let fun = JS_CallConstructor(ctx, ctx.getOpaque().Function_ctor, - cint(paramList.len), addr paramList[0]) + let fun = JS_CallConstructor(ctx, ctx.getOpaque().valRefs[jsvFunction], + cint(paramList.len), paramList.toJSValueArray()) for param in paramList: JS_FreeValue(ctx, param) return fun @@ -234,7 +235,8 @@ proc toJS*[T](ctx: JSContext; s: set[T]): JSValue = var a = toJS(ctx, x) if JS_IsException(a): return a - let ret = JS_CallConstructor(ctx, ctx.getOpaque().Set_ctor, 1, addr a) + let ret = JS_CallConstructor(ctx, ctx.getOpaque().valRefs[jsvSet], 1, + a.toJSValueArray()) JS_FreeValue(ctx, a) return ret @@ -319,12 +321,12 @@ proc toJS(ctx: JSContext; j: JSValue): JSValue = proc toJS(ctx: JSContext; promise: EmptyPromise): JSValue = var resolving_funcs: array[2, JSValue] - let jsPromise = JS_NewPromiseCapability(ctx, addr resolving_funcs[0]) + let jsPromise = JS_NewPromiseCapability(ctx, resolving_funcs.toJSValueArray()) if JS_IsException(jsPromise): return JS_EXCEPTION promise.then(proc() = - var x = JS_UNDEFINED - let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, addr x) + let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, + JS_UNDEFINED.toJSValueArray()) JS_FreeValue(ctx, res) JS_FreeValue(ctx, resolving_funcs[0]) JS_FreeValue(ctx, resolving_funcs[1])) @@ -332,12 +334,13 @@ proc toJS(ctx: JSContext; promise: EmptyPromise): JSValue = proc toJS[T](ctx: JSContext; promise: Promise[T]): JSValue = var resolving_funcs: array[2, JSValue] - let jsPromise = JS_NewPromiseCapability(ctx, addr resolving_funcs[0]) + let jsPromise = JS_NewPromiseCapability(ctx, resolving_funcs.toJSValueArray()) if JS_IsException(jsPromise): return JS_EXCEPTION promise.then(proc(x: T) = - var x = toJS(ctx, x) - let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, addr x) + let x = toJS(ctx, x) + let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, + x.toJSValueArray()) JS_FreeValue(ctx, res) JS_FreeValue(ctx, x) JS_FreeValue(ctx, resolving_funcs[0]) @@ -346,7 +349,7 @@ proc toJS[T](ctx: JSContext; promise: Promise[T]): JSValue = proc toJS[T, E](ctx: JSContext; promise: Promise[Result[T, E]]): JSValue = var resolving_funcs: array[2, JSValue] - let jsPromise = JS_NewPromiseCapability(ctx, addr resolving_funcs[0]) + let jsPromise = JS_NewPromiseCapability(ctx, resolving_funcs.toJSValueArray()) if JS_IsException(jsPromise): return JS_EXCEPTION promise.then(proc(x: Result[T, E]) = @@ -355,7 +358,8 @@ proc toJS[T, E](ctx: JSContext; promise: Promise[Result[T, E]]): JSValue = JS_UNDEFINED else: toJS(ctx, x.get) - let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, unsafeAddr x) + let res = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, + x.toJSValueArray()) JS_FreeValue(ctx, res) JS_FreeValue(ctx, x) else: # err @@ -363,7 +367,8 @@ proc toJS[T, E](ctx: JSContext; promise: Promise[Result[T, E]]): JSValue = JS_UNDEFINED else: toJS(ctx, x.error) - let res = JS_Call(ctx, resolving_funcs[1], JS_UNDEFINED, 1, unsafeAddr x) + let res = JS_Call(ctx, resolving_funcs[1], JS_UNDEFINED, 1, + x.toJSValueArray()) JS_FreeValue(ctx, res) JS_FreeValue(ctx, x) JS_FreeValue(ctx, resolving_funcs[0]) @@ -376,8 +381,8 @@ proc toJS*(ctx: JSContext; err: JSError): JSValue = var msg = toJS(ctx, err.message) if JS_IsException(msg): return msg - let ctor = ctx.getOpaque().err_ctors[err.e] - let ret = JS_CallConstructor(ctx, ctor, 1, addr msg) + let ctor = ctx.getOpaque().errCtorRefs[err.e] + let ret = JS_CallConstructor(ctx, ctor, 1, msg.toJSValueArray()) JS_FreeValue(ctx, msg) return ret @@ -385,9 +390,9 @@ proc toJS*(ctx: JSContext; abuf: JSArrayBuffer): JSValue = return JS_NewArrayBuffer(ctx, abuf.p, abuf.len, abuf.dealloc, nil, false) proc toJS*(ctx: JSContext; u8a: JSUint8Array): JSValue = - var jsabuf = toJS(ctx, u8a.abuf) - let ctor = ctx.getOpaque().Uint8Array_ctor - let ret = JS_CallConstructor(ctx, ctor, 1, addr jsabuf) + let jsabuf = toJS(ctx, u8a.abuf) + let ctor = ctx.getOpaque().valRefs[jsvUint8Array] + let ret = JS_CallConstructor(ctx, ctor, 1, jsabuf.toJSValueArray()) JS_FreeValue(ctx, jsabuf) return ret diff --git a/src/local/client.nim b/src/local/client.nim index 73b17c99..359d143c 100644 --- a/src/local/client.nim +++ b/src/local/client.nim @@ -35,6 +35,7 @@ import js/fromjs import js/intl import js/javascript import js/jstypes +import js/jsutils import js/module import js/timeout import js/tojs @@ -58,6 +59,7 @@ import chagashi/charset type Client* = ref object alive: bool + dead: bool config {.jsget.}: Config consoleWrapper: ConsoleWrapper fdmap: Table[int, Container] @@ -67,14 +69,13 @@ type pager {.jsget.}: Pager timeouts: TimeoutState pressed: tuple[col: int; row: int] + exitCode: int ConsoleWrapper = object console: Console container: Container prev: Container -jsDestructor(Client) - func console(client: Client): Console {.jsfget.} = return client.consoleWrapper.console @@ -90,12 +91,6 @@ template forkserver(client: Client): ForkServer = template readChar(client: Client): char = client.pager.term.readChar() -proc finalize(client: Client) {.jsfin.} = - if client.jsctx != nil: - free(client.jsctx) - if client.jsrt != nil: - free(client.jsrt) - proc fetch[T: Request|string](client: Client; req: T; init = none(RequestInit)): JSResult[FetchPromise] {.jsfunc.} = let req = ?newRequest(client.jsctx, req, init) @@ -155,19 +150,32 @@ proc suspend(client: Client) {.jsfunc.} = discard kill(0, cint(SIGTSTP)) client.pager.term.restart() -proc quit(client: Client; code = 0) {.jsfunc.} = - if client.alive: +proc jsQuit(client: Client; code = 0) {.jsfunc: "quit".} = + client.exitCode = code + client.alive = false + +proc quit(client: Client; code = 0) = + if not client.dead: + # dead is set to true when quit is called; it indicates that the + # client has been destroyed. + # alive is set to false when jsQuit is called; it is a request to + # destroy the client. + client.dead = true client.alive = false client.pager.quit() + for val in client.config.cmd.map.values: + JS_FreeValue(client.jsctx, val) + for fn in client.config.jsvfns: + JS_FreeValue(client.jsctx, fn) let ctx = client.jsctx - var global = JS_GetGlobalObject(ctx) - JS_FreeValue(ctx, global) - if client.jsctx != nil: - free(client.jsctx) - #TODO - #if client.jsrt != nil: - # free(client.jsrt) - quit(code) + let rt = client.jsrt + # Force the runtime to collect all memory, so QJS can check for + # leaks. + client[].reset() + GC_fullCollect() + ctx.free() + rt.free() + exitnow(code) proc feedNext(client: Client) {.jsfunc.} = client.feednext = true @@ -192,12 +200,11 @@ proc evalAction(client: Client; action: string; arg0: int32): EmptyPromise = p.resolve() if JS_IsFunction(ctx, ret): if arg0 != 0: - var arg0 = toJS(ctx, arg0) - let ret2 = JS_Call(ctx, ret, JS_UNDEFINED, 1, addr arg0) + let arg0 = toJS(ctx, arg0) + let ret2 = JS_Call(ctx, ret, JS_UNDEFINED, 1, arg0.toJSValueArray()) JS_FreeValue(ctx, arg0) JS_FreeValue(ctx, ret) ret = ret2 - JS_FreeValue(ctx, arg0) else: # no precnum let ret2 = JS_Call(ctx, ret, JS_UNDEFINED, 0, nil) JS_FreeValue(ctx, ret) @@ -514,11 +521,11 @@ proc handleError(client: Client; fd: int) = if client.pager.term.istream != nil and fd == client.pager.term.istream.fd: #TODO do something here... stderr.write("Error in tty\n") - quit(1) + client.quit(1) elif fd == client.forkserver.estream.fd: #TODO do something here... stderr.write("Fork server crashed :(\n") - quit(1) + client.quit(1) elif fd in client.loader.connecting: #TODO handle error? discard @@ -546,7 +553,7 @@ proc inputLoop(client: Client) = let selector = client.selector selector.registerHandle(int(client.pager.term.istream.fd), {Read}, 0) let sigwinch = selector.registerSignal(int(SIGWINCH), 0) - while true: + while client.alive: let events = client.selector.select(-1) for event in events: if Read in event.events: @@ -576,7 +583,7 @@ proc inputLoop(client: Client) = if not client.pager.hasload: # Failed to load every single URL the user passed us. We quit, and that # will dump all alerts to stderr. - quit(1) + client.quit(1) else: # At least one connection has succeeded, but we have nothing to display. # Normally, this means that the input stream has been redirected to a @@ -586,7 +593,7 @@ proc inputLoop(client: Client) = # loader, and then asking for confirmation if there is at least one. client.pager.term.setCursor(0, client.pager.term.attrs.height - 1) client.pager.term.anyKey("Hit any key to quit Chawan:") - quit(0) + client.quit(0) client.pager.showAlerts() client.pager.draw() @@ -598,7 +605,7 @@ func hasSelectFds(client: Client): bool = client.pager.procmap.len > 0 proc headlessLoop(client: Client) = - while client.hasSelectFds(): + while client.alive and client.hasSelectFds(): let events = client.selector.select(-1) for event in events: if Read in event.events: @@ -698,7 +705,7 @@ proc dumpBuffers(client: Client) = client.console.log("Error in buffer", $container.url) # check for errors client.handleRead(client.forkserver.estream.fd) - quit(1) + client.quit(1) proc launchClient*(client: Client; pages: seq[string]; contentType: Option[string]; cs: Charset; dump: bool) = @@ -824,13 +831,19 @@ proc newClient*(config: Config; forkserver: ForkServer; jsctx: JSContext; let loader = FileLoader(process: loaderPid, clientPid: getCurrentProcessId()) loader.setSocketDir(config.external.tmpdir) pager.setLoader(loader) - let client = Client(config: config, jsrt: jsrt, jsctx: jsctx, pager: pager) + let client = Client( + config: config, + jsrt: jsrt, + jsctx: jsctx, + pager: pager, + alive: true + ) jsrt.setInterruptHandler(interruptHandler, cast[pointer](client)) - var global = JS_GetGlobalObject(jsctx) jsctx.registerType(Client, asglobal = true) - setGlobal(jsctx, global, client) + jsctx.setGlobal(client) + let global = JS_GetGlobalObject(jsctx) jsctx.definePropertyE(global, "cmd", config.cmd.jsObj) - config.cmd.jsObj = JS_NULL JS_FreeValue(jsctx, global) + config.cmd.jsObj = JS_NULL client.addJSModules(jsctx) return client diff --git a/src/local/pager.nim b/src/local/pager.nim index 4ef4b4de..875f0df4 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -10,6 +10,7 @@ import std/tables import std/unicode import bindings/libregexp +import bindings/quickjs import config/config import config/mailcap import io/bufreader @@ -24,6 +25,7 @@ import js/error import js/fromjs import js/javascript import js/jstypes +import js/jsutils import js/regex import js/tojs import loader/connecterror @@ -175,6 +177,7 @@ proc setContainer*(pager: Pager; c: Container) {.jsfunc.} = pager.term.setTitle(c.getTitle()) proc hasprop(ctx: JSContext; pager: Pager; s: string): bool {.jshasprop.} = + result = false if pager.container != nil: let cval = toJS(ctx, pager.container) let val = JS_GetPropertyStr(ctx, cval, s) @@ -182,22 +185,29 @@ proc hasprop(ctx: JSContext; pager: Pager; s: string): bool {.jshasprop.} = result = true JS_FreeValue(ctx, val) -proc reflect(ctx: JSContext; this_val: JSValue; argc: cint; argv: ptr JSValue; - magic: cint; func_data: ptr JSValue): JSValue {.cdecl.} = - let fun = cast[ptr JSValue](cast[int](func_data) + sizeof(JSValue))[] - return JS_Call(ctx, fun, func_data[], argc, argv) +proc reflect(ctx: JSContext; this_val: JSValue; argc: cint; + argv: ptr UncheckedArray[JSValue]; magic: cint; + func_data: ptr UncheckedArray[JSValue]): JSValue {.cdecl.} = + let obj = func_data[0] + let fun = func_data[1] + return JS_Call(ctx, fun, obj, argc, argv) -proc getter(ctx: JSContext; pager: Pager; s: string): Option[JSValue] +proc getter(ctx: JSContext; pager: Pager; a: JSAtom): Option[JSValue] {.jsgetprop.} = if pager.container != nil: let cval = toJS(ctx, pager.container) - let val = JS_GetPropertyStr(ctx, cval, s) - if val != JS_UNDEFINED: - if JS_IsFunction(ctx, val): - var func_data = @[cval, val] - let fun = JS_NewCFunctionData(ctx, reflect, 1, 0, 2, addr func_data[0]) - return some(fun) + let val = JS_GetProperty(ctx, cval, a) + if JS_IsFunction(ctx, val): + let func_data = @[cval, val] + let fun = JS_NewCFunctionData(ctx, reflect, 1, 0, 2, + func_data.toJSValueArray()) + JS_FreeValue(ctx, cval) + JS_FreeValue(ctx, val) + return some(fun) + JS_FreeValue(ctx, cval) + if not JS_IsUndefined(val): return some(val) + return none(JSValue) proc searchNext(pager: Pager; n = 1) {.jsfunc.} = if pager.regex.isSome: @@ -880,8 +890,8 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset; continue if sc.rewrite_url.isSome: let fun = sc.rewrite_url.get - var arg1 = ctx.toJS(url) - let ret = JS_Call(ctx, fun, JS_UNDEFINED, 1, addr arg1) + var arg0 = ctx.toJS(url) + let ret = JS_Call(ctx, fun, JS_UNDEFINED, 1, arg0.toJSValueArray()) let nu = fromJS[URL](ctx, ret) if nu.isOk: if nu.get != nil: @@ -889,7 +899,7 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset; elif JS_IsException(ret): #TODO should writeException the message to console pager.alert("Error rewriting URL: " & ctx.getExceptionMsg(nu.error)) - JS_FreeValue(ctx, arg1) + JS_FreeValue(ctx, arg0) JS_FreeValue(ctx, ret) if sc.cookie.isSome: if sc.cookie.get: @@ -986,9 +996,11 @@ proc omniRewrite(pager: Pager; s: string): string = if rule.match.match(s): let fun = rule.substitute_url.get let ctx = pager.jsctx - var arg1 = ctx.toJS(s) - let jsRet = JS_Call(ctx, fun, JS_UNDEFINED, 1, addr arg1) + var arg0 = ctx.toJS(s) + let jsRet = JS_Call(ctx, fun, JS_UNDEFINED, 1, arg0.toJSValueArray()) let ret = fromJS[string](ctx, jsRet) + JS_FreeValue(ctx, jsRet) + JS_FreeValue(ctx, arg0) if ret.isOk: return ret.get pager.alert("Error in substitution of " & $rule.match & " for " & s & |