diff options
-rw-r--r-- | res/chawan.html | 6 | ||||
-rw-r--r-- | res/config.toml | 2 | ||||
-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 | ||||
-rw-r--r-- | test/js/class.html | 30 | ||||
-rw-r--r-- | test/js/encode_decode.html | 57 |
20 files changed, 360 insertions, 310 deletions
diff --git a/res/chawan.html b/res/chawan.html index 6ce6950b..07c76f84 100644 --- a/res/chawan.html +++ b/res/chawan.html @@ -68,9 +68,9 @@ up/down by one row <li><kbd>C-f</kbd>, <kbd>C-b</kbd> (or <kbd>PgDn</kbd>, <kbd>PgUp</kbd>)</kbd>: scroll up/down by an entire page <li><kbd>{number}G</kbd> (or <kbd>{number}gg</kbd>): jump to {number}'th line -<li><kbd>g0<kbd>: jump to first character of the current line's visible part -<li><kbd>gc<kbd>: jump to center of the current line's visible part -<li><kbd>g$<kbd>: jump to last character of the current line's visible part +<li><kbd>g0</kbd>: jump to first character of the current line's visible part +<li><kbd>gc</kbd>: jump to center of the current line's visible part +<li><kbd>g$</kbd>: jump to last character of the current line's visible part <li><kbd>{</kbd>, <kbd>}</kbd>: move cursor to the previous/next paragraph <li><kbd>-</kbd>, <kbd>+</kbd> (or <kbd>zh</kbd>, <kbd>zl</kbd>): shift screen to the left/right by one cell diff --git a/res/config.toml b/res/config.toml index a33b187b..21514c63 100644 --- a/res/config.toml +++ b/res/config.toml @@ -287,7 +287,7 @@ force-pixels-per-line = false [[omnirule]] match = '^ddg:' -substitute-url = '(x) => "https://lite.duckduckgo.com/lite/?kp=-1&kd=-1&q=" + encodeURIComponent(x.split(":").slice(1).join(":"))' +substitute-url = 'x => "https://lite.duckduckgo.com/lite/?kp=-1&kd=-1&q=" + encodeURIComponent(x.split(":").slice(1).join(":"))' [page] # buffer commands 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 & diff --git a/test/js/class.html b/test/js/class.html index 7c14049e..5ee2f869 100644 --- a/test/js/class.html +++ b/test/js/class.html @@ -1,23 +1,17 @@ <!doctype html> <title>Element class test</title> <div class="a b c">Fail</div> +<script src=asserts.js></script> <script> -(function() { - let div = document.getElementsByClassName("a")[0] - const classes = ["a", "b", "c"]; - let cl = div.classList; - for (let i = 0; i < classes.length; ++i) { - if (cl[i] !== classes[i]) - return; - } - const classes2 = ["x", "y", "z"]; - div.setAttribute("class", classes2.join(' ')); - let i = 0; - for (let x of cl) { - if (x != classes2[i]) - return; - ++i; - } - div.textContent = "Success"; -})(); +const div = document.getElementsByClassName("a")[0] +const classes = ["a", "b", "c"]; +let cl = div.classList; +for (let i = 0; i < classes.length; ++i) + assert_equals(cl[i], classes[i]); +const classes2 = ["x", "y", "z"]; +div.setAttribute("class", classes2.join(' ')); +let i = 0; +for (let x of cl) + assert_equals(x, classes2[i++]); +div.textContent = "Success"; </script> diff --git a/test/js/encode_decode.html b/test/js/encode_decode.html index 069ddc72..82e9676c 100644 --- a/test/js/encode_decode.html +++ b/test/js/encode_decode.html @@ -1,39 +1,32 @@ <!doctype html> <title>TextEncoder/TextDecoder test</title> <div id="success">Fail</div> +<script src=asserts.js></script> <script> -(function() { - /* Adapted from: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem */ - function base64ToBytes(base64) { - const binString = atob(base64); - const result = []; - for (const c of binString) - result.push(Uint8Array.from(c, (m) => m.codePointAt(0))); - return result; - } +/* Adapted from: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem */ +function base64ToBytes(base64) { + const binString = atob(base64); + const result = []; + for (const c of binString) + result.push(Uint8Array.from(c, (m) => m.codePointAt(0))); + return result; +} - function bytesToBase64(bytes) { - const binString = String.fromCodePoint(...bytes); - return btoa(binString); - } +function bytesToBase64(bytes) { + const binString = String.fromCodePoint(...bytes); + return btoa(binString); +} - const utf8 = new TextEncoder().encode("a Ā 𐀀 文 🦄") - const b64utf8 = bytesToBase64(utf8); - if (b64utf8 !== "YSDEgCDwkICAIOaWhyDwn6aE") { - console.log(b64utf8); - return; - } - const dec = new TextDecoder(); - const bytes = base64ToBytes(b64utf8); - const a = []; - let res = ""; - for (const c of bytes) - res += dec.decode(c, {stream: true}); - res += dec.decode(); - if (res !== "a Ā 𐀀 文 🦄") { - console.log(res); - return; - } - document.getElementById("success").textContent = "Success"; -})(); +const utf8 = new TextEncoder().encode("a Ā 𐀀 文 🦄") +const b64utf8 = bytesToBase64(utf8); +assert_equals(b64utf8, "YSDEgCDwkICAIOaWhyDwn6aE") +const dec = new TextDecoder(); +const bytes = base64ToBytes(b64utf8); +const a = []; +let res = ""; +for (const c of bytes) + res += dec.decode(c, {stream: true}); +res += dec.decode(); +assert_equals(res, "a Ā 𐀀 文 🦄"); +document.getElementById("success").textContent = "Success"; </script> |