Summary
A function wrapped with interop.FunctionReference and then passed to a native API as a block loses its FunctionReference identity. A later attempt to pass it as a function pointer then hits tns::Assert(false) in Interop::SetFFIParams, which crashes the app.
Repro
const fn = function () {};
const ref = new interop.FunctionReference(fn); // returns fn itself
// Any API taking a block, e.g.:
NSOperationQueue.mainQueue.addOperationWithBlock(ref);
// Any API taking a C function pointer, e.g. a struct field or a C function parameter
// typed as a function pointer:
someFunctionTakingAFunctionPointer(ref); // -> tns::Assert(false)
Cause
Both wrapper kinds live in the same per-object slot (tns::SetValue / tns::GetValue):
FunctionReference's constructor stores a FunctionReferenceWrapper on fn and registers fn with ObjectManager (FunctionReference.cpp).
- The block branch of
Interop::SetFFIParams doesn't recognise a FunctionReferenceWrapper as a cached block. It builds a JSBlock and overwrites the slot with its BlockWrapper. tns::SetValue doesn't free the previous wrapper, so the FunctionReferenceWrapper leaks, along with the trampoline it may have cached.
- The function-pointer branch accepts only
Pointer, AnonymousFunction and FunctionReference wrappers. With a BlockWrapper in the slot it falls through to tns::Assert(false, isolate) (NativeScript/runtime/Interop.mm, function-pointer branch of SetFFIParams).
In the opposite order (block first, then new interop.FunctionReference(fn)), the constructor overwrites the block's wrapper. Every later block marshal of fn then builds a new block instead of reusing the cached one, and the next one overwrites the FunctionReferenceWrapper again.
Expected
One function can be used both as a block and as a function pointer. Possible directions:
- keep the block cache and the FunctionReference state in separate slots;
- let the block path recognise a
FunctionReferenceWrapper and keep the block alongside it.
Either way, neither marshal should evict the other's wrapper, and the function-pointer path should never assert on a wrapper type it can explain to the user. At minimum it should throw a JS error instead.
Context
Found while reviewing #500, which makes JS block wrappers owned by their JSBlock. The worker test added there (blockFunctionReferenceWorker.js) relies on the current overwrite behaviour to reach the teardown path.
Summary
A function wrapped with
interop.FunctionReferenceand then passed to a native API as a block loses its FunctionReference identity. A later attempt to pass it as a function pointer then hitstns::Assert(false)inInterop::SetFFIParams, which crashes the app.Repro
Cause
Both wrapper kinds live in the same per-object slot (
tns::SetValue/tns::GetValue):FunctionReference's constructor stores aFunctionReferenceWrapperonfnand registersfnwithObjectManager(FunctionReference.cpp).Interop::SetFFIParamsdoesn't recognise aFunctionReferenceWrapperas a cached block. It builds aJSBlockand overwrites the slot with itsBlockWrapper.tns::SetValuedoesn't free the previous wrapper, so theFunctionReferenceWrapperleaks, along with the trampoline it may have cached.Pointer,AnonymousFunctionandFunctionReferencewrappers. With aBlockWrapperin the slot it falls through totns::Assert(false, isolate)(NativeScript/runtime/Interop.mm, function-pointer branch ofSetFFIParams).In the opposite order (block first, then
new interop.FunctionReference(fn)), the constructor overwrites the block's wrapper. Every later block marshal offnthen builds a new block instead of reusing the cached one, and the next one overwrites theFunctionReferenceWrapperagain.Expected
One function can be used both as a block and as a function pointer. Possible directions:
FunctionReferenceWrapperand keep the block alongside it.Either way, neither marshal should evict the other's wrapper, and the function-pointer path should never assert on a wrapper type it can explain to the user. At minimum it should throw a JS error instead.
Context
Found while reviewing #500, which makes JS block wrappers owned by their
JSBlock. The worker test added there (blockFunctionReferenceWorker.js) relies on the current overwrite behaviour to reach the teardown path.