reearth / quickjs-emscripten-sync

Provides a way to sync objects between browser and QuickJS
MIT License
62 stars 7 forks source link

Support for async/Promises #3

Closed i404788 closed 2 years ago

i404788 commented 2 years ago

Hey there,

I've had a blast using your library but noticed promises don't really work (resolving them inside QuickJS). Is there a limitation on this, or is it possible to use them?

Thanks

Error for reference: TypeError: Promise.prototype.then called on incompatible Proxy

rot1024 commented 2 years ago

Promise is currently not supported; Promsie is a special object and needs to be verified to work well, including how asynchronous processing between different runtimes should be implemented. 

i404788 commented 2 years ago

I did some more experiments trying to use it as an Opaque object, and it does seem to work in some cases.

Doing await evalCode('v = foreignAsyncFunc(); return v') works with no problem.

However doing the same with a callback doesn't work:

const depromisify = (x: Promise<T>, cb: (T) => any) => {x.then(cb)}
arena.expose({depromisify, foreignAsyncFunc})
evalCode('v = foreignAsyncFunc(); depromisify(v, console.log)')`

Is the marshalling different between evalCode and FFIs?

i404788 commented 2 years ago

It took a bit of effort but I found a way to 'depromisify' classes and functions:

export function depromisifyFn<T>(fn: (...args: any[]) => Promise<T>, cls: any):
    (...args: any[]) => (void|any) {
  return (...args: any[]) => {
    const cb: (v: T) => void = args[args.length - 1] 
    //console.log("Depromisifying call", fn, cls, cb)
    const promise = fn.apply(cls, args.slice(0, args.length - 1))
    //console.log(fn, promise)
    if (!cb) {
      return promise
    }
    if (promise && typeof promise.then === 'function' && promise[Symbol.toStringTag] === 'Promise') {
      // is compliant native promise implementation
      promise.then(cb).catch(cb)
    }
    else {
      return promise
    }
  }
}

export function depromisifyClass(cls: any): any {
  const excluded = ["constructor"]
  let proxy: any = null
  const wrapValue = (target: any, key: any, tag: string) => {
      //console.log(tag, cls, target, key)
      if (!excluded.includes(key)) {
        if (typeof target[key] == "function") {
          return depromisifyFn(target[key].bind(cls), cls)
        }
      }
      return target[key]
  }

  const prototype_handler = {
    getOwnPropertyDescriptor(target: any, prop: string) {
      let desc: any = Object.getOwnPropertyDescriptor(target, prop)
      desc.value = wrapValue(desc, 'value', 'proto_descriptor')
      //console.log(desc)
      return desc
    },
    get(target: any, key: string) {
      return wrapValue(target, key, 'proto')
    }
  }

  const prototype_proxy = new Proxy(Object.getPrototypeOf(cls), prototype_handler as any)

  const handler = {
    getPrototypeOf(target: any){
      return prototype_proxy
    },
    get(target: any, key: string) {
      if (key == '__proto__') return prototype_proxy

      return wrapValue(target, key, 'self')
    }
  }
  proxy = new Proxy(cls, handler)
  return proxy
}
justjake commented 2 years ago

I can’t follow what “depromisify” means, but maybe this can help: In the base library, I recently added QuickJSVm.resolvePromise which turns a VM promise into a host promise. This was the missing dual to QuickJSVm.newPromise, which is how the host can construct a promise inside the guest.

The error path makes promise handling tricky - if a VM promise rejection is unmarshalled to a host promise rejection, the unmarshalling layer must be sure to free the rejection reason handle to avoid a leak. For this reason QuickJSVm.resolvePromise always resolves with a result object (and never rejects) to ensure the developer must consider the error handle.

I think building a bit of custom handling of values that are instanceof Promise into quickjs-emscripten-sync would be a good idea for that reason.

i404788 commented 2 years ago

The depromisify function is specifically for the sync library, it will warp/proxy classes/functions (and classes' prototypes) such that the last argument will become a callback, and no Promise is returned unless no callback is added. This is the inverse functionality of the shim function promisify.

I think it would be a lot better to use proper quickjs Promise handling though, since depromisify does make callback hell very common and it has some edge-cases due to the way it detects arguments.

rot1024 commented 2 years ago

Thank you guys. It's good news that QuickJSVM.resolvePromise and QuickJSVM.newPromise has been added to quickjs-emscripten. I'll try to implement it soon.

atifsyedali commented 2 years ago

Any updates?

Matthiee commented 2 years ago

Would be nice to have this feature! 👍

rot1024 commented 2 years ago

v1.4 has been released. quickjs-emscripten v0.20~ and promises are supported. Check it!