swiftwasm/JavaScriptKit

Occasional JSClosure has been already released by Swift side

vkhougaz-vifive opened this issue · 4 comments

During development of an animated webassembly feature a function that takes and returns a large typed array started failing occasionally with an error similar to:

The JSClosure has been already released by Swift side. The closure is created at ...

This occurs approximately 50% of frames

I've tried to create a minimal reproduction here, details in the README:

https://github.com/vkhougaz-vifive/swiftwasm-bug

import Foundation

import JavaScriptKit

func reverseArray(bytes: [Float32]) -> [Float32] {
    return [Float32](bytes.reversed())
}

let jsClosure = JSClosure { (input: [JSValue]) in
    let bytes: [Float32] = try! JSValueDecoder().decode(from: input[0])

    return reverseArray(bytes: bytes).jsValue
}

@_cdecl("main")
func main(_ i: Int32, _ j: Int32) -> Int32 {
    JSObject.global.reverseFloat32Array = .object(jsClosure)

    return 0
}
window.onload = async () => {
  const output = document.getElementById("output")!;

  function* generator() {
    for (let step = 0; step < 10000; step++) {
      yield Math.random();
    }
  }

  await loadWasm(bugWasm);

  function animate() {
    try {
      const bytes = Float32Array.from(generator());

      const reversed = reverseFloat32Array(bytes);

      output.innerHTML = reversed.join("\n");
    } catch (e) {
      console.error(e);
    }
    requestAnimationFrame(animate);
  }
  animate();
};

The hacky patch included there is... concerning, pointing towards either a dramatic misunderstanding of how swift works or a crazy low level memory issue.

/// Returns true if the host function has been already released, otherwise false.
@_cdecl("_call_host_function_impl")
func _call_host_function_impl(
    _ hostFuncRef: JavaScriptHostFuncRef,
    _ argv: UnsafePointer<RawJSValue>, _ argc: Int32,
    _ callbackFuncRef: JavaScriptObjectRef
) -> Bool {
    // TODO: This is some sort of horrible hack due to some sort of horrible wasm thing
    // Otherwise the sharedClone SOMETIMES fails
    let sharedClone = Dictionary(uniqueKeysWithValues: zip(JSClosure.sharedClosures.keys, JSClosure.sharedClosures.values))

    guard let (_, hostFunc) = sharedClone[hostFuncRef] else {
        return true
    }
    let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map(\.jsValue)
    let result = hostFunc(arguments)
    let callbackFuncRef = JSFunction(id: callbackFuncRef)
    _ = callbackFuncRef(result)
    return false
}

Thank you for detail report and repro. It looks like our runtime has something wrong. Let me check it

I'm still running into this issue indexing a dict using a string passed in via JSValue

Is it possible that strings in wasm allocated (heap?) memory cannot be used to index dictionaries, but strings on the stack can? hmn.

OK, the root cause is that you are using command line model instead of reactor model. JavaScriptKit only supports reactor model. I've sent you a patch vkhougaz-vifive/swiftwasm-bug#1

We will add more friendly diagnostics to avoid this situation.
See also carton code: https://github.com/swiftwasm/carton/blob/b8cac7dc8e9c96e95406bfc5312768a2e659bd81/Sources/SwiftToolchain/Toolchain.swift#L368-L373

Okay, I don't know why I was having so many issues but I fixed the compilation command and it's working well. Thanks!

swift build --triple wasm32-unknown-wasi -c $MODE -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=main