diff options
-rw-r--r-- | doc/lib.rst | 3 | ||||
-rw-r--r-- | lib/js/jsffi.nim | 436 | ||||
-rw-r--r-- | tests/js/tjsffi.nim | 267 | ||||
-rw-r--r-- | web/website.ini | 2 |
4 files changed, 707 insertions, 1 deletions
diff --git a/doc/lib.rst b/doc/lib.rst index 8bb602b78..6b498e696 100644 --- a/doc/lib.rst +++ b/doc/lib.rst @@ -415,6 +415,9 @@ Modules for JS backend * `dom <dom.html>`_ Declaration of the Document Object Model for the JS backend. +* `jsffi <jsffi.html>`_ + Types and macros for easier interaction with JavaScript. + Deprecated modules ------------------ diff --git a/lib/js/jsffi.nim b/lib/js/jsffi.nim new file mode 100644 index 000000000..646b79fe5 --- /dev/null +++ b/lib/js/jsffi.nim @@ -0,0 +1,436 @@ +# +# +# Nim's Runtime Library +# (c) Copyright 2017 Nim Authors +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +## This Module implements types and macros to facilitate the wrapping of, and +## interaction with JavaScript libraries. Using the provided types ``JsObject`` +## and ``JsAssoc`` together with the provided macros allows for smoother +## interfacing with JavaScript, allowing for example quick and easy imports of +## JavaScript variables: +## +## .. code-block:: nim +## +## # Here, we are using jQuery for just a few calls and do not want to wrap the +## # whole library: +## +## # import the document object and the console +## var document {. importc, nodecl .}: JsObject +## var console {. importc, nodecl .}: JsObject +## # import the "$" function +## proc jq(selector: JsObject): JsObject {. importcpp: "$(#)" .} +## +## # Use jQuery to make the following code run, after the document is ready. +## # This uses an experimental ``.()`` operator for ``JsObject``, to emit +## # JavaScript calls, when no corresponding proc exists for ``JsObject``. +## proc main = +## jq(document).ready(proc() = +## console.log("Hello JavaScript!") +## ) +## + +when not defined(js) and not defined(nimdoc) and not defined(nimsuggest): + {.fatal: "Module jsFFI is designed to be used with the JavaScript backend.".} + +import macros, tables + +const + setImpl = "#[#] = #" + getImpl = "#[#]" + +var + mangledNames {. compileTime .} = initTable[string, string]() + nameCounter {. compileTime .} = 0 + +proc validJsName(name: string): bool = + result = true + const reservedWords = ["break", "case", "catch", "class", "const", "continue", + "debugger", "default", "delete", "do", "else", "export", "extends", + "finally", "for", "function", "if", "import", "in", "instanceof", "new", + "return", "super", "switch", "this", "throw", "try", "typeof", "var", + "void", "while", "with", "yield", "enum", "implements", "interface", + "let", "package", "private", "protected", "public", "static", "await", + "abstract", "boolean", "byte", "char", "double", "final", "float", "goto", + "int", "long", "native", "short", "synchronized", "throws", "transient", + "volatile", "null", "true", "false"] + case name + of reservedWords: return false + else: discard + if name[0] notin {'A'..'Z','a'..'z','_','$'}: return false + for chr in name: + if chr notin {'A'..'Z','a'..'z','_','$','0'..'9'}: + return false + +template mangleJsName(name: cstring): cstring = + inc nameCounter + "mangledName" & $nameCounter + +type + JsRoot* = ref object of RootObj + ## Root type of both JsObject and JsAssoc + JsObject* = ref object of JsRoot + ## Dynamically typed wrapper around a JavaScript object. + JsAssoc*[K, V] = ref object of JsRoot + ## Statically typed wrapper around a JavaScript object. + NotString = concept c + c isnot string + +# New +proc newJsObject*: JsObject {. importcpp: "{@}" .} + ## Creates a new empty JsObject +proc newJsAssoc*[K, V]: JsAssoc[K, V] {. importcpp: "{@}" .} + ## Creates a new empty JsAssoc with key type `K` and value type `V`. + +# Checks +proc hasOwnProperty*(x: JsObject, prop: cstring): bool + {. importcpp: "#.hasOwnProperty(#)" .} + ## Checks, whether `x` has a property of name `prop`. + +proc jsTypeOf*(x: JsObject): cstring {. importcpp: "typeof(#)" .} + ## Returns the name of the JsObject's JavaScript type as a cstring. + +# Conversion to and from JsObject +proc to*(x: JsObject, T: typedesc): T {. importcpp: "(#)" .} + ## Converts a JsObject `x` to type `T`. +proc toJs*[T](val: T): JsObject {. importcpp: "(#)" .} + ## Converts a value of any type to type JsObject + +proc `[]`*(obj: JsObject, field: cstring): JsObject {. importcpp: getImpl .} + ## Return the value of a property of name `field` from a JsObject `obj`. + +proc `[]=`*[T](obj: JsObject, field: cstring, val: T) {. importcpp: setImpl .} + ## Set the value of a property of name `field` in a JsObject `obj` to `v`. + +proc `[]`*[K: NotString, V](obj: JsAssoc[K, V], field: K): V + {. importcpp: getImpl .} + ## Return the value of a property of name `field` from a JsAssoc `obj`. + +proc `[]`*[V](obj: JsAssoc[string, V], field: cstring): V + {. importcpp: getImpl .} + ## Return the value of a property of name `field` from a JsAssoc `obj`. + +proc `[]=`*[K: NotString, V](obj: JsAssoc[K, V], field: K, val: V) + {. importcpp: setImpl .} + ## Set the value of a property of name `field` in a JsAssoc `obj` to `v`. + +proc `[]=`*[V](obj: JsAssoc[string, V], field: cstring, val: V) + {. importcpp: setImpl .} + ## Set the value of a property of name `field` in a JsAssoc `obj` to `v`. + +proc `==`*(x, y: JsRoot): bool {. importcpp: "(# === #)" .} + ## Compare two JsObjects or JsAssocs. Be careful though, as this is comparison + ## like in JavaScript, so if your JsObjects are in fact JavaScript Objects, + ## and not strings or numbers, this is a *comparison of references*. + +{. experimental .} +macro `.`*(obj: JsObject, field: static[cstring]): JsObject = + ## Experimental dot accessor (get) for type JsObject. + ## Returns the value of a property of name `field` from a JsObject `x`. + ## + ## Example: + ## + ## .. code-block:: nim + ## + ## let obj = newJsObject() + ## obj.a = 20 + ## console.log(obj.a) # puts 20 onto the console. + if validJsName($field): + let importString = "#." & $field + result = quote do: + proc helper(o: JsObject): JsObject + {. importcpp: `importString`, gensym .} + helper(`obj`) + else: + if not mangledNames.hasKey($field): + mangledNames[$field] = $mangleJsName(field) + let importString = "#." & mangledNames[$field] + result = quote do: + proc helper(o: JsObject): JsObject + {. importcpp: `importString`, gensym .} + helper(`obj`) + +macro `.=`*(obj: JsObject, field: static[cstring], value: untyped): untyped = + ## Experimental dot accessor (set) for type JsObject. + ## Sets the value of a property of name `field` in a JsObject `x` to `value`. + if validJsName($field): + let importString = "#." & $field & " = #" + result = quote do: + proc helper(o: JsObject, v: auto) + {. importcpp: `importString`, gensym .} + helper(`obj`, `value`) + else: + if not mangledNames.hasKey($field): + mangledNames[$field] = $mangleJsName(field) + let importString = "#." & mangledNames[$field] & " = #" + result = quote do: + proc helper(o: JsObject, v: auto) + {. importcpp: `importString`, gensym .} + helper(`obj`, `value`) + +macro `.()`*(obj: JsObject, field: static[cstring], + args: varargs[JsObject, toJs]): JsObject = + ## Experimental "method call" operator for type JsObject. + ## Takes the name of a method of the JavaScript object (`field`) and calls + ## it with `args` as arguments, returning a JsObject (which may be discarded, + ## and may be `undefined`, if the method does not return anything, + ## so be careful when using this.) + ## + ## Example: + ## + ## .. code-block:: nim + ## + ## # Let's get back to the console example: + ## var console {. importc, nodecl .}: JsObject + ## let res = console.log("I return undefined!") + ## console.log(res) # This prints undefined, as console.log always returns + ## # undefined. Thus one has to be careful, when using + ## # JsObject calls. + var importString: string + if validJsName($field): + importString = "#." & $field & "(@)" + else: + if not mangledNames.hasKey($field): + mangledNames[$field] = $mangleJsName(field) + importString = "#." & mangledNames[$field] & "(@)" + result = quote do: + proc helper(o: JsObject): JsObject + {. importcpp: `importString`, gensym .} + helper(`obj`) + for idx in 0 ..< args.len: + let paramName = newIdentNode(!("param" & $idx)) + result[0][3].add newIdentDefs(paramName, newIdentNode(!"JsObject")) + result[1].add args[idx].copyNimTree + +macro `.`*[K: string | cstring, V](obj: JsAssoc[K, V], + field: static[cstring]): V = + ## Experimental dot accessor (get) for type JsAssoc. + ## Returns the value of a property of name `field` from a JsObject `x`. + var importString: string + if validJsName($field): + importString = "#." & $field + else: + if not mangledNames.hasKey($field): + mangledNames[$field] = $mangleJsName(field) + importString = "#." & mangledNames[$field] + result = quote do: + proc helper(o: type(`obj`)): `obj`.V + {. importcpp: `importString`, gensym .} + helper(`obj`) + +macro `.=`*[K: string | cstring, V](obj: JsAssoc[K, V], + field: static[cstring], value: V): untyped = + ## Experimental dot accessor (set) for type JsAssoc. + ## Sets the value of a property of name `field` in a JsObject `x` to `value`. + var importString: string + if validJsName($field): + importString = "#." & $field & " = #" + else: + if not mangledNames.hasKey($field): + mangledNames[$field] = $mangleJsName(field) + importString = "#." & mangledNames[$field] & " = #" + result = quote do: + proc helper(o: type(`obj`), v: `obj`.V) + {. importcpp: `importString`, gensym .} + helper(`obj`, `value`) + +macro `.()`*[K: string | cstring, V: proc](obj: JsAssoc[K, V], + field: static[cstring], args: varargs[untyped]): auto = + ## Experimental "method call" operator for type JsAssoc. + ## Takes the name of a method of the JavaScript object (`field`) and calls + ## it with `args` as arguments. Here, everything is typechecked, so you do not + ## have to worry about `undefined` return values. + let dotOp = bindSym"." + result = quote do: + (`dotOp`(`obj`, `field`))() + for elem in args: + result[0].add elem + +# Iterators: + +iterator pairs*(obj: JsObject): (cstring, JsObject) = + ## Yields tuples of type ``(cstring, JsObject)``, with the first entry + ## being the `name` of a fields in the JsObject and the second being its + ## value wrapped into a JsObject. + var k: cstring + var v: JsObject + {.emit: "for (var `k` in `obj`) {".} + {.emit: " if (!`obj`.hasOwnProperty(`k`)) continue;".} + {.emit: " `v`=`obj`[`k`];".} + yield (k, v) + {.emit: "}".} + +iterator items*(obj: JsObject): JsObject = + ## Yields the `values` of each field in a JsObject, wrapped into a JsObject. + var v: JsObject + {.emit: "for (var k in `obj`) {".} + {.emit: " if (!`obj`.hasOwnProperty(k)) continue;".} + {.emit: " `v`=`obj`[k];".} + yield v + {.emit: "}".} + +iterator keys*(obj: JsObject): cstring = + ## Yields the `names` of each field in a JsObject. + var k: cstring + {.emit: "for (var `k` in `obj`) {".} + {.emit: " if (!`obj`.hasOwnProperty(`k`)) continue;".} + yield k + {.emit: "}".} + +iterator pairs*[K, V](assoc: JsAssoc[K, V]): (K,V) = + ## Yields tuples of type ``(K, V)``, with the first entry + ## being a `key` in the JsAssoc and the second being its corresponding value. + when K is string: + var k: cstring + else: + var k: K + var v: V + {.emit: "for (var `k` in `assoc`) {".} + {.emit: " if (!`assoc`.hasOwnProperty(`k`)) continue;".} + {.emit: " `v`=`assoc`[`k`];".} + when K is string: + yield ($k, v) + else: + yield (k, v) + {.emit: "}".} + +iterator items*[K,V](assoc: JSAssoc[K,V]): V = + ## Yields the `values` in a JsAssoc. + var v: V + {.emit: "for (var k in `assoc`) {".} + {.emit: " if (!`assoc`.hasOwnProperty(k)) continue;".} + {.emit: " `v`=`assoc`[k];".} + yield v + {.emit: "}".} + +iterator keys*[K,V](assoc: JSAssoc[K,V]): K = + ## Yields the `keys` in a JsAssoc. + when K is string: + var k: cstring + else: + var k: K + {.emit: "for (var `k` in `assoc`) {".} + {.emit: " if (!`assoc`.hasOwnProperty(`k`)) continue;".} + when K is string: + yield $k + else: + yield k + {.emit: "}".} + +# Literal generation + +macro `{}`*(typ: typedesc, xs: varargs[untyped]): auto = + ## Takes a ``typedesc`` as its first argument, and a series of expressions of + ## type ``key: value``, and returns a value of the specified type with each + ## field ``key`` set to ``value``, as specified in the arguments of ``{}``. + ## + ## Example: + ## + ## .. code-block:: nim + ## + ## # Let's say we have a type with a ton of fields, where some fields do not + ## # need to be set, and we do not want those fields to be set to ``nil``: + ## type + ## ExtremelyHugeType = ref object + ## a, b, c, d, e, f, g: int + ## h, i, j, k, l: cstring + ## # And even more fields ... + ## + ## let obj = ExtremelyHugeType{ a: 1, k: "foo".cstring, d: 42 } + ## + ## # This generates roughly the same JavaScript as: + ## {. emit: "var obj = {a: 1, k: "foo", d: 42};" .} + ## + let a = !"a" + var body = quote do: + var `a` {.noinit.}: `typ` + {.emit: "`a` = {};".} + for x in xs.children: + if x.kind == nnkExprColonExpr: + let + k = x[0] + kString = quote do: + when compiles($`k`): $`k` else: "invalid" + v = x[1] + body.add(quote do: + when compiles(`a`.`k`): + `a`.`k` = `v` + elif compiles(`a`[`k`]): + `a`[`k`] = `v` + else: + `a`[`kString`] = `v` + ) + else: + error("Expression `" & $x.toStrLit & "` not allowed in `{}` macro") + + body.add(quote do: + return `a` + ) + + result = quote do: + proc inner(): `typ` {.gensym.} = + `body` + inner() + +# Macro to build a lambda using JavaScript's `this` +# from a proc, `this` being the first argument. + +macro bindMethod*(procedure: typed): auto = + ## Takes the name of a procedure and wraps it into a lambda missing the first + ## argument, which passes the JavaScript builtin ``this`` as the first + ## argument to the procedure. Returns the resulting lambda. + ## + ## Example: + ## + ## We want to generate roughly this JavaScript: + ## + ## .. code-block:: js + ## var obj = {a: 10}; + ## obj.someMethod = function() { + ## return this.a + 42; + ## }; + ## + ## We can achieve this using the ``bindMethod`` macro: + ## + ## .. code-block:: nim + ## let obj = JsObject{ a: 10 } + ## proc someMethodImpl(that: JsObject): int = + ## that.a.to(int) + 42 + ## obj.someMethod = bindMethod someMethodImpl + ## + ## # Alternatively: + ## obj.someMethod = bindMethod + ## proc(that: JsObject): int = that.a.to(int) + 42 + if not (procedure.kind == nnkSym or procedure.kind == nnkLambda): + error("Argument has to be a proc or a symbol corresponding to a proc.") + var + rawProc = if procedure.kind == nnkSym: + getImpl(procedure.symbol) + else: + procedure + args = rawProc[3] + thisType = args[1][1] + params = newNimNode(nnkFormalParams).add(args[0]) + body = newNimNode(nnkLambda) + this = newIdentNode("this") + # construct the `this` parameter: + thisQuote = quote do: + var `this` {. nodecl, importc .} : `thisType` + call = newNimNode(nnkCall).add(rawProc[0], thisQuote[0][0][0][0]) + # construct the procedure call inside the method + if args.len > 2: + for idx in 2..args.len-1: + params.add(args[idx]) + call.add(args[idx][0]) + body.add(newNimNode(nnkEmpty), + rawProc[1], + rawProc[2], + params, + rawProc[4], + rawProc[5], + newTree(nnkStmtList, thisQuote[0], call) + ) + result = body diff --git a/tests/js/tjsffi.nim b/tests/js/tjsffi.nim new file mode 100644 index 000000000..71eb211e3 --- /dev/null +++ b/tests/js/tjsffi.nim @@ -0,0 +1,267 @@ +discard """ + output: '''true +true +true +true +true +true +true +true +true +true +true +true +true +true +true +true''' +""" + +import macros, jsffi + +# Tests for JsObject +# Test JsObject []= and [] +block: + proc test(): bool = + let obj = newJsObject() + var working = true + obj["a"] = 11 + obj["b"] = "test" + obj["c"] = "test".cstring + working = working and obj["a"].to(int) == 11 + working = working and obj["c"].to(cstring) == "test".cstring + working + echo test() + +# Test JsObject .= and . +block: + proc test(): bool = + let obj = newJsObject() + var working = true + obj.a = 11 + obj.b = "test" + obj.c = "test".cstring + obj.`$!&` = 42 + obj.`while` = 99 + working = working and obj.a.to(int) == 11 + working = working and obj.b.to(string) == "test" + working = working and obj.c.to(cstring) == "test".cstring + working = working and obj.`$!&`.to(int) == 42 + working = working and obj.`while`.to(int) == 99 + working + echo test() + +# Test JsObject .() +block: + proc test(): bool = + let obj = newJsObject() + obj.`?!$` = proc(x, y, z: int, t: string): string = t & $(x + y + z) + obj.`?!$`(1, 2, 3, "Result is: ").to(string) == "Result is: 6" + echo test() + +# Test JsObject []() +block: + proc test(): bool = + let obj = newJsObject() + obj.a = proc(x, y, z: int, t: string): string = t & $(x + y + z) + let call = obj["a"].to(proc(x, y, z: int, t: string): string) + call(1, 2, 3, "Result is: ") == "Result is: 6" + echo test() + +# Test JsObject Iterators +block: + proc testPairs(): bool = + let obj = newJsObject() + var working = true + obj.a = 10 + obj.b = 20 + obj.c = 30 + for k, v in obj.pairs: + case $k + of "a": + working = working and v.to(int) == 10 + of "b": + working = working and v.to(int) == 20 + of "c": + working = working and v.to(int) == 30 + else: + return false + working + proc testItems(): bool = + let obj = newJsObject() + var working = true + obj.a = 10 + obj.b = 20 + obj.c = 30 + for v in obj.items: + working = working and v.to(int) in [10, 20, 30] + working + proc testKeys(): bool = + let obj = newJsObject() + var working = true + obj.a = 10 + obj.b = 20 + obj.c = 30 + for v in obj.keys: + working = working and $v in ["a", "b", "c"] + working + proc test(): bool = testPairs() and testItems() and testKeys() + echo test() + +# Test JsObject equality +block: + proc test(): bool = + {. emit: "var comparison = {a: 22, b: 'test'};" .} + var comparison {. importc, nodecl .}: JsObject + let obj = newJsObject() + obj.a = 22 + obj.b = "test".cstring + obj.a == comparison.a and obj.b == comparison.b + echo test() + +# Test JsObject literal +block: + proc test(): bool = + {. emit: "var comparison = {a: 22, b: 'test'};" .} + var comparison {. importc, nodecl .}: JsObject + let obj = JsObject{ a: 22, b: "test".cstring } + obj.a == comparison.a and obj.b == comparison.b + echo test() + +# Tests for JsAssoc +# Test JsAssoc []= and [] +block: + proc test(): bool = + let obj = newJsAssoc[int, int]() + var working = true + obj[1] = 11 + working = working and not compiles(obj["a"] = 11) + working = working and not compiles(obj["a"]) + working = working and not compiles(obj[2] = "test") + working = working and not compiles(obj[3] = "test".cstring) + working = working and obj[1] == 11 + working + echo test() + +# Test JsAssoc .= and . +block: + proc test(): bool = + let obj = newJsAssoc[string, int]() + var working = true + obj.a = 11 + obj.`$!&` = 42 + working = working and not compiles(obj.b = "test") + working = working and not compiles(obj.c = "test".cstring) + working = working and obj.a == 11 + working = working and obj.`$!&` == 42 + working + echo test() + +# Test JsAssoc .() +block: + proc test(): bool = + let obj = newJsAssoc[string, proc(e: int): int]() + obj.a = proc(e: int): int = e * e + obj.a(10) == 100 + echo test() + +# Test JsAssoc []() +block: + proc test(): bool = + let obj = newJsAssoc[string, proc(e: int): int]() + obj.a = proc(e: int): int = e * e + let call = obj["a"] + call(10) == 100 + echo test() + +# Test JsAssoc Iterators +block: + proc testPairs(): bool = + let obj = newJsAssoc[string, int]() + var working = true + obj.a = 10 + obj.b = 20 + obj.c = 30 + for k, v in obj.pairs: + case $k + of "a": + working = working and v == 10 + of "b": + working = working and v == 20 + of "c": + working = working and v == 30 + else: + return false + working + proc testItems(): bool = + let obj = newJsAssoc[string, int]() + var working = true + obj.a = 10 + obj.b = 20 + obj.c = 30 + for v in obj.items: + working = working and v in [10, 20, 30] + working + proc testKeys(): bool = + let obj = newJsAssoc[string, int]() + var working = true + obj.a = 10 + obj.b = 20 + obj.c = 30 + for v in obj.keys: + working = working and v in ["a", "b", "c"] + working + proc test(): bool = testPairs() and testItems() and testKeys() + echo test() + +# Test JsAssoc equality +block: + proc test(): bool = + {. emit: "var comparison = {a: 22, b: 55};" .} + var comparison {. importcpp, nodecl .}: JsAssoc[string, int] + let obj = newJsAssoc[string, int]() + obj.a = 22 + obj.b = 55 + obj.a == comparison.a and obj.b == comparison.b + echo test() + +# Test JsAssoc literal +block: + proc test(): bool = + {. emit: "var comparison = {a: 22, b: 55};" .} + var comparison {. importcpp, nodecl .}: JsAssoc[string, int] + let obj = JsAssoc[string, int]{ a: 22, b: 55 } + var working = true + working = working and + compiles(JsAssoc[int, int]{ 1: 22, 2: 55 }) + working = working and + comparison.a == obj.a and comparison.b == obj.b + working = working and + not compiles(JsAssoc[string, int]{ a: "test" }) + working + echo test() + +# Tests for macros on non-JsRoot objects +# Test lit +block: + type TestObject = object + a: int + b: cstring + proc test(): bool = + {. emit: "var comparison = {a: 1};" .} + var comparison {. importc, nodecl .}: TestObject + let obj = TestObject{ a: 1 } + obj == comparison + echo test() + +# Test bindMethod +block: + type TestObject = object + a: int + onWhatever: proc(e: int): int + proc handleWhatever(that: TestObject, e: int): int = + e + that.a + proc test(): bool = + let obj = TestObject(a: 9, onWhatever: bindMethod(handleWhatever)) + obj.onWhatever(1) == 10 + echo test() diff --git a/web/website.ini b/web/website.ini index 0405acd08..a3a04f01e 100644 --- a/web/website.ini +++ b/web/website.ini @@ -39,7 +39,7 @@ srcdoc2: "impure/re;impure/nre;pure/typetraits" srcdoc2: "pure/concurrency/threadpool.nim;pure/concurrency/cpuinfo.nim" srcdoc: "system/threads.nim;system/channels.nim;js/dom" srcdoc2: "pure/os;pure/strutils;pure/math;pure/matchers;pure/algorithm" -srcdoc2: "pure/stats;impure/nre;windows/winlean;pure/random" +srcdoc2: "pure/stats;impure/nre;windows/winlean;pure/random;js/jsffi" srcdoc2: "pure/complex;pure/times;pure/osproc;pure/pegs;pure/dynlib;pure/strscans" srcdoc2: "pure/parseopt;pure/parseopt2;pure/hashes;pure/strtabs;pure/lexbase" srcdoc2: "pure/parsecfg;pure/parsexml;pure/parsecsv;pure/parsesql" |