I'm having fun with implementing externrefs support in wasmrt, and wanted to ask for some ideas on how to do it.
__externref_t is a C type, that holds a reference to external (javascript) object. It allows for fast js/wasm interop, avoiding JS glue to store refs solely on the JS side. The problem with this type though is that it can only be used as a local variable, function argument, or function return type, and that's it. To store a ref one would need to put it in a table (lets call this primitive proc storeRef(t: ExternRef): int), and later retrieve it (proc retrieveRef(idx: int): ExternRef). Ideally these two operations should be avoided when ExternRef doesn't escape stack. So I came up with the following type system:
type
JSExternRef {.importc: "__externref_t", noinit.} = object
JSExternObj[T] = distinct JSExternRef
JSObj* = object # Used to represent a stored externref, can be subclassed by user
idx: int Now users can freely subclass JSObj, e.g.
type
HTMLElement* = object of JSObj
Canvas* = object of HTMLElement Also, users can write bindings to js, like so:
proc append(a, b: HTMLElement) {.importwasmm.} importwasmm would bind to method append, but it silently changes all JSObj types to JSExternRef counterparts, so under the hood, this proc will look smth like:
proc append(a, b: JSExternObj[HTMLElement]) = ... This works nicely because there are implicit conversions (with predefined converters) between JSExternRef and JSObj, utilizing mentioned above primitives. The idea here is that most of the time the users will get away with JSExternRefs, and these conversions won't happen. E.g.
let d = document() # this function is declared to return Document, but in fact returns JSExternObj[Document]
d.write("foo") # d is passed to another js function, we're not storing refs anywhere.
myStruct.document = d # only here the conversion happens, causing store operation.
Now it all works nicely, until we pass a subclass:
var h = getSomeHtmlElement() # h is JSExternObj[HtmlElement]
var c = createCanvas() # c is JSExternObj[Canvas]
h.append(c) # Error here! second argument is of wrong type! Even though Canvas is a subclass of HTMLElement, JSExternObj[Canvas] is not a subclass of JSExternRef[HTMLElement]. Theoretically a converter with generic return type would help, e.g.
converter toExternObj[From, To](v: JSExternObj[From]): JSExternObj[To] = JSExternRef[To](v) But nim doesn't allow that. So I'd welcome your ideas on how to achieve this, ideally without letting the user bother about explicitly storing refs, and without requiring them to utilize macros to define their types, like so:
# instead of this
type HTMLElement = JSObj
# do this
defineJSType(HTMLElement, JSObj) # Macro defines all the needed converters along with the type, but looks pretty ugly.