# Monoucha manual **IMPORTANT**: currently, Monoucha only works correctly with refc, ergo you *must* to put `--mm:refc` in your `nim.cfg`. ORC cannot deal with Monoucha's GC-related hacks, and if you use ORC, you will run into memory errors on larger projects. I hope to fix this in the future. For now, please use refc. **UNDER CONSTRUCTION**: this document is an incomplete draft. Partially untested code and possible inaccuracies ahead. ## Table of Contents * [Introduction](#introduction) - [Hello, world](#hello-world) - [Error handling](#error-handling) * [Registering objects](#registering-objects) - [registerType: registering type interfaces](#registertype-registering-type-interfaces) * [Global objects](#global-objects) * [Inheritance](#inheritance) * [Misc registerType parameters](#misc-registertype-parameters) - [jsget, jsset: basic property reflectors](#jsget-jsset-basic-property-reflection) - [Non-reference objects](#non-reference-objects) * [Function pragmas](#function-pragmas) - [jsfunc: regular functions](#jsfunc-regular-functions) - [jsctor: constructors](#jsctor-constructors) - [jsfget, jsfset: custom property reflectors](#jsfget-jsfset-custom-property-reflectors) - [jsstfunc: static functions](#jsstfunc-static-functions) - [jsuffunc, jsufget, jsuffget: the LegacyUnforgeable property](#jsuffunc-jsufget-jsuffget-the-legacyunforgeable-property) - [jsgetownprop, jsgetprop, jssetprop, jsdelprop, jshasprop, jspropnames: magic functions](#jsgetownprop-jsgetprop-jssetprop-jsdelprop-jshasprop-jspropnames-magic-functions) - [jsfin: object finalizers](#jsfin-object-finalizers) * [toJS, fromJS](#tojs-fromjs) - [Using raw JSValues](#using-raw-jsvalues) - [Using toJS](#using-tojs) - [Using fromJS](#using-fromjs) - [Custom type converters](#custom-type-converters) ## Introduction Monoucha is a high-level wrapper to QuickJS. It was created for the [Chawan](https://sr.ht/~bptato/chawan) browser to avoid manually writing bindings to JS APIs. While Monoucha *is* high-level, it does not try to completely abstract away the low-level details. You will in many cases have to use QuickJS APIs directly to achieve something; Monoucha only provides abstractions to APIs where doing something manually would be tedious and/or error-prone. Also note that Monoucha is *not* complete, and neither is QuickJS-NG. While a major API break for documented interfaces is unlikely, it may happen at any time in the future. Please pin a specific version if you need a stable API. ### Hello, world Let's start with a simplified version of the example from the README: ```nim import monoucha/fromjs import monoucha/javascript let rt = newJSRuntime() let ctx = rt.newJSContext() const code = "'Hello from JS!'" let val = ctx.eval(code) var res: string assert ctx.fromJS(val, res).isSome # no error echo res # Hello from JS! JS_FreeValue(ctx, val) ctx.free() rt.free() ``` This is the minimal required code to run a script. You may notice a few things: * eval() takes two parameters, one for code and one for the file name. The file name will be used for exception formatting. * You have to free the context and runtime handles manually. This is unfortunately unavoidable; the good news is that you won't have to do much manual memory management after this. The `res` variable then holds a QuickJS JSValue. In this case, we convert it to a string before freeing it. You can skip conversion if you don't care about the script's return value, but you must *always* free it. You may be thinking to yourself, "why is there no convenience wrapper around this?" The reason is illustrated in the next section. ### Error handling Let's get ourselves a ReferenceError: ```nim const code = "abcd" let val = ctx.eval(code) ``` If you try to convert this into a string, you will get an err() result. Obviously, you want to print your errors *somewhere*, but Monoucha does not care how and where you log error messages, and leaves this task to the user. However, there is an easy way to retrieve the current error message: ```nim if JS_IsException(res): stderr.writeLine(ctx.getExceptionMsg()) ``` In most cases, you should wrap `eval` in a function that deals with exceptions in the most appropriate way for your application. Alternatively, a self-contained evalConvert can be written as follows: ```nim import results import monoucha/tojs proc evalConvert[T](ctx: JSContext; code: string; file = ""): Result[T, string] = let val = ctx.eval(code, file, flags) var res: T if ctx.fromJS(val, res).isNone: # Exception when converting the value. JS_FreeValue(ctx, val) return err(ctx.getExceptionMsg()) JS_FreeValue(ctx, val) # All ok! Return the converted object. return ok(res) ``` This is less efficient than immediately logging the exception message. ## Registering objects So far we have talked about running JS code and getting its result, which is not enough for most use cases. If you are embedding QuickJS, you probably want some sort of interoperability between JS and Nim code. In JavaScript, all objects are passed *by reference*. Monoucha allows you to transparently use Nim object references in JS, provided you register their type interface first. ### registerType: registering type interfaces To register object types as a JavaScript interface, you must call the `registerType` macro on the JS context with a type of your choice. ```nim macro registerType*(ctx: JSContext; t: typed; parent: JSClassID = 0; asglobal: static bool = false; nointerface = false; name: static string = ""; hasExtraGetSet: static bool = false; extraGetSet: static openArray[TabGetSet] = []; namespace = JS_NULL; errid = opt(JSErrorEnum)): JSClassID ``` Typically, you would do this using Nim reference types. Non-reference types work too, but have some restrictions which will be covered. Now for the first example. Following code registers a JS interface for the Nim ref object `Moon`: ```nim type Moon = ref object jsDestructor(Moon) # [...] ctx.registerType(Moon) const code = "Moon" let val = ctx.eval(code) var res: string assert ctx.fromJS(val, res).isSome # no error echo res # function Moon() [...] JS_FreeValue(ctx, val) ``` Quite straightforward: just call `registerType`. Pay attention to the jsDestructor template: you call jsDestructor immediately after your type declaration *before* any other functions, or Monoucha will complain. (This is necessary so that we can generate a `=destroy` hook for the object.) #### Global objects `registerType` also allows you to change the global object's type. This is quite important, as it is the only way to create global functions (discounting object constructors). To register a global type, set `asglobal = true` in `registerType`: ```nim type Earth = ref object # [...] let earth = Earth() ctx.registerType(Earth, asglobal = true) ctx.setGlobal(earth) const code = "assert(globalThis instanceof Earth)" let val = ctx.eval(code) assert not JS_IsException(val) JS_FreeValue(ctx, val) ``` You may notice two things: * We call `setGlobal` with an instance of Earth. This is needed to register some object as the backing Nim object; this same instance of Earth will be passed to bound functions. * This time, we do not call jsDestructor, because the global object is special-cased; its reference is kept until the JS context gets freed. Therefore it does not need a `=destroy` hook. #### Inheritance `registerType` also allows you to specify inheritance chains by setting the `parent` parameter: ```nim type Planet = ref object of RootObj Earth = ref object of Planet Moon = ref object of Planet # [...] let planetCID = ctx.registerType(Planet) ctx.registerType(Earth, parent = planetCID, asglobal = true) ctx.registerType(Moon, parent = planetCID) ctx.setGlobal(Earth()) # make sure to set a global so global functions work const code = "assert(globalThis instanceof Planet)" let val = ctx.eval(code) assert not JS_IsException(val) JS_FreeValue(ctx, val) ``` In this model, the inheritance tree looks like: * Planet - Earth - Moon There is no strict requirement to actually model the Nim inheritance chain. e.g. if we set "Rock" as the parent of Planet, then we could use Rock as the direct ancestor of Earth without even registering Planet at all. However, this is a two-edged blade, as it also allows specifying invalid models which may result in undefined behavior. For example, setting Earth as the `parent` of Moon compiles, but is invalid, as it will result in "Moon" Nim objects being casted to Earth references. #### Misc registerType parameters Following parameters also exist: * `nointerface`: suppress constructor creation * `name`: set a different JS name than the Nim name * `hasExtraGetSet`, `extraGetSet`: an array of magic getters/setters. `hasExtraGetSet` must be set to true in case you pass anything in `extraGetSet` because of a compiler bug. * `namespace`: instead of defining the constructor on the global object, define it on the passed JS object. You must use QuickJS APIs to create this object. `namespace` is not consumed (i.e. you must free it yourself). ### jsget, jsset: basic property reflectors Time to actually expose some Nim values to JS. The `jsget` and `jsset` pragmas can be set on fields of registered object types to directly expose them to JS: ```nim type Moon = ref object Earth = ref object moon {.jsget.}: Moon name {.jsgetset.}: string population {.jsset.}: int64 jsDestructor(Moon) # [...] let earth = Earth(moon: Moon(), population: 1, name: "Earth") ctx.registerType(Earth, asglobal = true) ctx.registerType(Moon) ctx.setGlobal(earth) const code = """ globalThis.population = 8e9; "name: " + globalThis.name + ", moon: " + globalThis.moon; """ let val = ctx.eval(code) var res: string assert ctx.fromJS(val, res).isSome # no error echo res # name: Earth, moon: [object Moon] echo earth.population # 8e9 JS_FreeValue(ctx, val) ``` In the above example, we expose an Earth instance as the global object, and modify/inspect it. By default, object fields are not exposed to JS; `{.jsget.}` gives JS read-only access, `{.jsset.}` write-only, and `jsgetset` expands to `{.jsget, jsset.}` (both read and write). ### Non-reference objects JavaScript objects have reference semantics, so this does not make much sense at first glance. However, children of heap-allocated objects do in fact have a permanent address, which we can convert to JS so long as we hold a reference to their parent. e.g. this works: ```nim type Moon = object Earth = ref object moon {.jsget.}: Moon jsDestructor(Moon) # [...] let earth = Earth(moon: Moon()) ctx.registerType(Earth, asglobal = true) ctx.registerType(Moon) ctx.setGlobal(earth) const code = "globalThis.moon" let val = ctx.eval(code) var res: string assert ctx.fromJS(val, res).isSome # no error echo res # [object Moon] JS_FreeValue(ctx, val) ``` This still has some restrictions: for example, you cannot return a non-reference object from a [wrapped](#function-pragmas) function. ## Function pragmas The main feature of Monoucha is that it can automatically wrap functions and expose them to JavaScript by associating them with types exposed through [registerType](#registertype-registering-type-interfaces). All you have to do is to stick pragmas to function definitions. Here we enumerate over all currently available pragmas. ### jsfunc: regular functions The simplest pragma is `.jsfunc`. This marks the function as a member of the JS interface associated with the first parameter's type. Example: ```nim type Window = ref object console {.jsget.}: Console Console = ref object jsDestructor(Console) proc log(console: Console; s: string) {.jsfunc.} = echo s # [...] let window = Window(console: Console()) ctx.registerType(Window, asglobal = true) ctx.registerType(Console) ctx.setGlobal(window) const code = "console.log('Hello, world!')" JS_FreeValue(ctx, ctx.eval(code)) ``` As you can see, `log` has been exposed as a member of the JS interface `Console`. It is possible to use a different name for the JS function than for the Nim procedure. e.g. the following will also expose a `log` function: ```nim proc jsLog(console: Console; s: string) {.jsfunc: "log".} = # [...] ``` In general, you can use any combination of parameters in `.jsfunc` procs. These are converted on a best-effort basis: e.g. in the above example, `console.log(1)` would pass the string "1", not an exception. Monoucha tries to adhere to the WebIDL standard in this regard. (TODO: find & document places where this is not true yet.) The first parameter *must* be a reference type that has been registered using `registerType`. Alternatively, you can also use a registered non-reference object type, but in this case, you *must* annotate it with `var`: ```nim type Console2 = object # not ref! proc log(console: var Console2; s: string) {.jsfunc.} = # [...] ``` It is also possible to insert a "zeroeth" parameter to get a reference to the current JS context. This is useful if you want to access state global to the JS context without storing a backreference to the global object: ```nim proc log(ctx: JSContext; console: Console; s: string) {.jsfunc.} = # This assumes you have already setGlobal a Window instance. let global = JS_GetGlobalObject(ctx) var window: Window assert ctx.fromJS(global, window).isSome # no error JS_FreeValue(ctx, global) # Now you can do something with the window, e.g. window.outFile.writeLine(s) ``` It is also possible to use `varargs` in `.jsfunc` functions: ```nim proc log(ctx: JSContext; console: Console; ss: varargs[JSValueConst]) {.jsfunc.} = discard # can be called like `console.log("a", "b", "c", "d")` ``` For efficiency reasons, only `JSValueConst` varargs are supported. (In the past, union types and non-JSValueConst varargs also worked. This feature was dropped because it generated inefficient and bloated code; `fromJS` with `JSValueConst` parameters can be used to the same effect.) For further information about individual type conversions, see the [toJS, fromJS](#tojs-fromjs) section. ### jsctor: constructors The `.jsctor` pragma is used to define a constructor for a specific type: ```nim type JSFile = ref object path {.jsget.}: string jsDestructor(JSFile) proc newJSFile(path: string): JSFile {.jsctor.} = return JSFile(path: path) # [...] # Use different name in JS through `name': File in JS is mapped to JSFile # in Nim. ctx.registerType(JSFile, name = "File") const code = "console.log(new File('/path/to/file'))" JS_FreeValue(ctx, ctx.eval(code)) # [object File] ``` `.jsctor`, like other pragmas, supports the same "zeroeth" JSContext parameter trick as [jsfunc](#jsfunc-regular-functions), which is useful when the global object is needed for resource allocation. ### jsfget, jsfset: custom property reflectors The `.jsfget` and `.jsfset` pragmas can be used to define custom getter/setter functions. Like `.jsget` and `.jsset`, they appear as regular getters and setters in JS. However, instead of automatically reflecting a property, `.jsfget` and `.jsfset` allows you to write custom code to handle property accesses. Example: ```nim # [...] (see above for constructor) func name(file: JSFile): string {.jsfget.} = return file.path.substr(file.path.rfind('/') + 1) proc setName(file: JSFile; s: string) {.jsfset: "name".} = let i = file.path.rfind('/') file.path = file.path.substr(0, i) & s # [...] const code = """ const file = new JSFile("/path/to/file"); console.log(file.path); /* /path/to/file */ console.log(file.name); /* file */ file.name = "new-name"; console.log(file.path); /* /path/to/new-name */ """ JS_FreeValue(ctx, ctx.eval(code)) ``` ### jsstfunc: static functions `.jsstfunc` defines a static function on a given interface. Unlike with `.jsfunc`, you must provide at least a single parameter for these functions, with the syntax `Interface.functionName`. Note that `Interface` must be an interface registered through `registerType`. If the interface was renamed, the Nim name (*not* the JS name) must be used. Example: ```nim # [...] (see above for constructor) proc jsExists(path: string): bool {.jsstfunc: "JSFile.exists".} = return fileExists(path) # [...] const code = """ console.log(File.exists("doc/manual.md")); /* true */ """ JS_FreeValue(ctx, ctx.eval(code)) ``` ### jsuffunc, jsufget, jsuffget: the LegacyUnforgeable property The pragmas `.jsuffunc`, `.jsufget` and `.jsuffget` correspond to the WebIDL [`[LegacyUnforgeable]`](https://webidl.spec.whatwg.org/#LegacyUnforgeable) property. Concretely, this means that the function (or getter) is defined on *instances* of the interface, not on the interface (i.e. object prototype) as a non-configurable property. Even more concretely, this means that the function (or getter) cannot be changed by JavaScript code. ```nim # this will always return the result of the fstat call. proc owner(file: JSFile): int {.jsuffget.} = let fd = open(cstring(file.path), O_RDONLY, 0) if fd == -1: return -1 var stats = Stat.default if fstat(fd, stats) == -1: discard close(fd) return -1 return int(stats.st_uid) proc getOwner(file: JSFile): int {.jsuffget.} = return file.owner # [...] const code = """ const file = new File("doc/manual.md"); const oldGetOwner = file.getOwner; file.getOwner = () => -2; /* doesn't work */ assert(oldGetOwner == file.getOwner); Object.defineProperty(file, "owner", { value: -2 }); /* throws */ """ JS_FreeValue(ctx, ctx.eval(code)) ``` ### jsgetownprop, jsgetprop, jssetprop, jsdelprop, jshasprop, jspropnames: magic functions `.jsgetownprop`, `.jsgetprop`, `.jssetprop`, `.jsdelprop`, `.jshasprop` and `.jspropnames` generate bindings for magic functions. These are mainly useful for collections, where you want to provide custom behavior for property accesses. (TODO elaborate...) ### jsfin: object finalizers The `.jsfin` pragma can be used to clean up resources used by objects at the end of their lifetime. In principle, this is just like the Nim `=destroy` property, except it also tracks the lifetime of possible JS objects which the Nim object may back. (In other words, it's a cross-GC finalizer.) The first parameter must be a reference to the object in question. Only one `.jsfin` procedure per reference type is allowed, and `.jsfin` is *not* inherited. This means that you must set up separate `.jsfin` functions for each child object in the inheritance chain. `.jsfin` also supports a "zeroeth" parameter, but here it must be a `JSRuntime`, *not* `JSContext`. WARNING: this parameter is nil when -an object that was not bound to a JS value is finalized. (e.g. calling toJS on the object, or returning the object from a `.jsfunc` converts it to a JSValue too.) WARNING 2: like Nim `=destroy`, this pragma is very easy to misuse. In particular, make sure to **NEVER ALLOCATE** in a `.jsfin` finalizer, because this [breaks](https://github.com/nim-lang/Nim/issues/4851) Nim refc. (I don't know if this problem is still present in ORC, but at the moment Monoucha does not work with ORC anyway.) Example: ```nim type JSFile = ref object path: string buffer: pointer # some internal buffer handled as managed memory jsDestructor(JSFile) proc newJSFile(path: string): JSFile {.jsctor.} = return JSFile( path: path, buffer: alloc(4096) ) var unrefd {.global.} = 0 proc finalize(file: JSFile) {.jsfin.} = if file.buffer != nil: dealloc(file.buffer) # Note: it is not necessary to nil out the pointer; it's just me being # paranoid :P file.buffer = nil inc unrefd # [...] const code = """ { /* following call is in a separate code, so QJS can unref it * immediately. */ const file = new File("doc/manual.md"); } /* in contrast, following file will not be deallocated until the runtime is * gone. */ const file = new File("doc/manual.md"); """ JS_FreeValue(ctx, ctx.eval(code)) GC_fullCollect() # ensure refc runs assert unrefd == 1 # first file is already deallocated ctx.free() GC_fullCollect() # ensure refc runs assert unrefd == 1 # the second file is still available rt.free() assert unrefd == 2 # runtime is freed, so the second file gets deallocated too ``` ## toJS, fromJS This section covers the handling and conversion of JSValue types. While in most cases it is possible to avoid using JSValues, Monoucha does not go out of its way to completely eliminate them. In particular, handling JSValues is unavoidable when: * You want to do something with `eval()`'s result. * You need to call a QuickJS API not wrapped by Monoucha. (e.g. JS function calls) * You want a dynamically typed variable, e.g. for "union" types. ### Using raw JSValues When passing around raw JSValues, it is important to make sure you reference/unreference appropriately. For this, use the `JS_DupValue` and `JS_FreeValue` functions from QuickJS. (When you only have access to a `JSRuntime`, use the `JS_FreeValueRT` and `JS_DupValueRT` variants instead.) Note the presence of JSValueConst; this is a distinct subtype of JSValue that indicates that the value is borrowed. It is analogous to the `lent` keyword in Nim (which is implicit in procedure parameters). In contrast, procedures that take a non-const JSValue are expected to take ownership of said JSValue and eventually free it. This behavior is anologous to the `sink` keyword in Nim. To get raw JSValues in `.jsfunc` (or similar) bound functions, you can simply set the desired parameter's type to `JSValueConst`. This way, you get a "borrowed" JSValue; to keep a reference to these after the function exits, reference them with `JS_DupValue` first. (Analogously, you do not have to free such JSValues as long as you don't call `JS_DupValue` on them, either.) Since JSValues need a JSContext to do anything useful, you may want to set the first parameter of such functions to a `JSContext` type; this passes the current JSContext on to the bound function. (For details, see [above](#jsfunc-regular-functions).) ### Using toJS ```nim proc toJS[T](ctx: JSContext; val: T): JSValue ``` Monoucha internally uses the overloaded `toJS` function to convert bound function return values to JS values. This is available to user code too; simply import the `monoucha/tojs` module. Naturally, `JSValue`s you get from toJS are owned by you, so you should call `JS_FreeValue` on these when you no longer need them. The `tojs` module also includes some other convenience functions: * `defineProperty`, `definePropertyC`, `definePropertyE`, `definePropertyCW`, `definePropertyCWE`: simple wrappers around `JS_DefineProperty*` functions from the QuickJS API. Unlike the QuickJS versions, they panic on errors, so only use these if you are 100% sure that they always succeed.
The `C`, `E`, `CW`, `CWE` represent the "configurable", "enumerable", and "writable" flags of the property.
Warning: like in QuickJS, these functions *consume* a JSValue; that is, if you pass a JSValue, then the function will call `JS_FreeValue` on it. * `newFunction`: creates a new JavaScript function. `args` is a list of parameter names, `body` is the JavaScript function body. ### Using fromJS ```nim proc fromJS[T](ctx: JSContext; val: JSValueConst; res: var T): Err[void] ``` `fromJS` is the opposite of `toJS`: it converts `JSValue`s into Nim values. Import the `monoucha/fromjs` module to use it. On success, `fromJS` fills `res` and returns `Opt[void].some()`. On failure, `res` is set to an unspecified value, a QuickJS exception is thrown (using `JS_Throw()`), and `Opt[void].none()` is returned. **Warning**: JSDict in general is somewhat finnicky: you must make sure that their destructors run before deinitializing the runtime. In practice, this means a) you must not use JSDict in the same procedure where you free the JSRuntime, b) you must call GC_fullCollect before freeing the runtime if you use JSDict. (TODO: this all seems very broken. Why isn't JSDict itself just a ref object?) Passing `JS_EXCEPTION` to `fromJS` is valid, and results in no new exception being thrown. ### Custom type converters In Monoucha, object reference types are automatically converted to JS reference types. However, value types are different: * A non-reference `object` is converted to a JS reference by implicitly turning it into `ptr object`, as noted [above](#non-reference-objects). * Trying to pass any other type to/from JS errors out at compilation. To work around this limitation, you can override `toJS` and `fromJS` for specific types. In both cases, it is enough to add an overload for the respective function and expose it to the module where the converter is needed (i.e. where you call `registerType`).