as-bind icon indicating copy to clipboard operation
as-bind copied to clipboard

add support for exported classes

Open mathe42 opened this issue 4 years ago • 4 comments

I currently look at how we can support classes.

Example:

Assemblyscript

export class example {
  constructor(private s: string) {}

  public getString() { return this.s }
}

In JS:

const e = new exports.example('answer: 42')
console.log(e.getString()) // "answer: 42"

This in combination with #64 would allow some cool things!

To do that I would wrap the full class in a JS-Class that calls the correspondig function by runtime and loader.

FinalizationRegistry

I want to do the following to get full garbage collection (in a wrapper function for the classes):

const classPtr = new exports.Foo()
exports.__pin(classPtr)
const foo = Foo.wrap(classPtr)

// We only need one of these for all classes
const registry = new FinalizationRegistry(ptr => {
   exports.__unpin(ptr)
});

registry.register(foo, classPtr);

This would allow full GarbageCollection for Browsers that support FinalizationRegistry (see https://caniuse.com/mdn-javascript_builtins_finalizationregistry). Without FinalizationRegistry I see no way to add this wrapping without memory leaks (any ideas?).

But in that case the developer can allways call __unpin so I think there is nothing to worry about.

mathe42 avatar Sep 29 '21 16:09 mathe42

Or instead of wrapping useing a proxy.

mathe42 avatar Sep 29 '21 16:09 mathe42

Got a small working prototype (all manual but can be automated):

Assemblyscript

export class Foo {
  constructor(public str: string) {}

  getString(): string {
    return this.str
  }
}

Host:

import * as AsBind from "/node_modules/as-bind/dist/as-bind.esm.js";

const asyncTask = async () => {
  const wasm = await fetch("/build/optimized.wasm").then(v => v.arrayBuffer());

  const asBindInstance = await AsBind.instantiate(wasm);

  let e = asBindInstance.exports

  // When not supported noop
  const pointerRegistry = FinalizationRegistry ? new FinalizationRegistry(ptr => {
    e.__unpin(ptr)
  }) : {register(){}};

  function createProxyClass(klass, definition) {
    const newConstructor = new Proxy(klass, {
      construct(target, args) {
        // TODO: wrap args - get from definition
        const ptr = new target(...args)
        return newConstructor.wrap(ptr)
      },
      get(_, prop) {
        if (prop === 'wrap') {
          return (ptr) => {
            e.__pin(ptr)

            const instance = klass.wrap(ptr)

            const a = new Proxy({}, {
              ownKeys() {
                return Reflect.ownKeys(instance);
              },
              defineProperty() {
                throw new Error("Not allowed!")
              },
              deleteProperty() {
                throw new Error("Not allowed!")
              },
              get(_, ...args) {
                if (prop === '__collect') {
                  return () => e.__unpin(ptr)
                }
                const value = Reflect.get(instance, ...args)
                // TODO: wrap function - or wrap value
                return value
              },
              set(_, ...args) {
                // TODO: check if is settable; wrap value if needed
                return Reflect.set(instance, ...args)
              },
              setPrototypeOf() {
                throw new Error("Not allowed!")
              },
              preventExtensions() {
                throw new Error("Not allowed!")
              },
              isExtensible() {
                return false;
              },
              has(_, p) {
                return Reflect.has(instance, p)
              },
              getPrototypeOf() {
                throw new Error("Not allowed!")
              }
            })

            // handle GC
            pointerRegistry.register(a, ptr)

            return a
          }
        }
        return undefined
      },
      set() {
        throw new Error("Not allowed!")
      }
    })

    return newConstructor
  }


  window.e = e
  window.Foo = createProxyClass(e.Foo, {
    constructor: {
      memberType: "constructor",
      parameterTypes: ["~lib/string/String"],
      returnType: null
    },
    str: {
      memberType: "field",
      set: true,
      get: true,
      type: "~lib/string/String"
    },
    getString: {
      memberType: "function",
      parameterTypes: [],
      returnType: "~lib/string/String"
    }
  })

  // example usage
  const sptr = e.__newString("test")
  const f = new Foo(sptr)
  console.log(e.__getString(f.getString())) // "test"
  const sptr2 = e.__newString("test 42")
  f.str = sptr2
  console.log(e.__getString(f.getString())) // "test 42"
};
asyncTask();

mathe42 avatar Sep 29 '21 17:09 mathe42

@torch2424 If this looks good to you I will integrate it. :D

mathe42 avatar Sep 29 '21 19:09 mathe42

@mathe42 Thank you very much for drafting up this implementation! 😄 Yeah! I like this approach and it makes sense to me, so let's go for it! 😄

torch2424 avatar Sep 30 '21 20:09 torch2424