Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Native properties provided by internal classes are not available #1149

Closed
mbektchiev opened this issue May 30, 2019 · 5 comments
Closed

Native properties provided by internal classes are not available #1149

mbektchiev opened this issue May 30, 2019 · 5 comments
Assignees
Milestone

Comments

@mbektchiev
Copy link
Contributor

mbektchiev commented May 30, 2019

Environment

  • iOS Runtime: 5.3.0+

Describe the bug
There are cases in the iOS SDK where the public headers information (which is the source of NativeScript's metadata) and the actual internal classes which comprise the APIs are different. E.g. Let's consider the inconsistencies of downloadTaskWithURL:completionHandler: method of NSURLSession:

  • The instance that it actually returns is of some internal interface type __NSCFLocalDownloadTask (tested on iOS 12.1)
  • This interface doesn't even inherit from the class advertised as the type of the method's return value -- NSURLSessionDownloadTask. Its base class hierarchy is: __NSCFLocalDownloadTask -> __NSCFLocalSessionTask -> __NSCFURLSessionTask -> NSURLSessionTask -> NSObject
  • NSURLSessionDownloadTask doesn't implement the response selector

=> As a result, the iOS runtime hides this property (along with several others) and reports them as undefined.

To Reproduce

Execute the following JavaScript:

const url = NSURL.URLWithString("http://upload.wikimedia.org/wikipedia/commons/7/7f/Williams_River-27527.jpg");

const downloadPhotoTask = NSURLSession.sharedSession.downloadTaskWithURLCompletionHandler(url, () => {
          console.log("response1: ", downloadPhotoTask.performSelector("response"));
          console.log("response2: ", downloadPhotoTask.response);
});
downloadPhotoTask.resume();

=> The first log correctly prints the response object. While the second one prints undefined.

Expected behavior
Both logs should be identical.

Additional context
The issue has been introduced with the additional filtering implemented with #1086.

Discovered with NativeScript/nativescript-background-http#214

Workaround
As a workaround, such methods, property getters or setters can be invoked dynamically via performSelector, performSelector:withObject: or performSelector:withObject:withObject:

When the return value is not an Objective-C object the above methods will not work correctly. In such cases, NSInvocation should be used like it's done here: https://github.com/NativeScript/nativescript-background-http/pull/222/files#diff-4d82b00cf4fca680654c7e489bc635b2R235

@lambourn
Copy link

lambourn commented Jun 7, 2019

Glad to see this fixed, we're facing the same with NSURLSessionTask.currentRequest

@mbektchiev
Copy link
Contributor Author

mbektchiev commented Jun 7, 2019

@lambourn Actually we still haven't fixed the bug in the runtime. You can temporarily switch to the performSelector workaround until we find a proper fix for the bug, without regressing the fixes which introduced this issue.

@lambourn
Copy link

lambourn commented Jun 7, 2019

@mbektchiev ok, I'll rephrase: "I'll be happy once this is fixed" ;-)

We use the performSelector now as a workaround.

Once more, it's great that {N} offers this kind of flexibility to work with the underlying runtime. Otherwise it would be a show stopper.

@tbozhikov
Copy link
Contributor

Hey guys, just to add more on workarounds - in some cases performSelector does not help and returns nothing for properties that are there. In such cases, you can utilize NSInvocation like in the following helper class:

class NativePropertyReader {
    private _invocationCache = new Map<string, NSInvocation>();

    private getInvocationObject(object: NSObject, selector: string): NSInvocation {
        let invocation = this._invocationCache.get(selector);
        if (!invocation) {
            const sig = object.methodSignatureForSelector(selector);
            invocation = NSInvocation.invocationWithMethodSignature(sig);
            invocation.selector = selector;

            this._invocationCache[selector] = invocation;
        }

        return invocation;
    }

    public readProp<T>(object: NSObject, prop: string, type: interop.Type<T>): T {
        const invocation = this.getInvocationObject(object, prop);
        invocation.invokeWithTarget(object);

        const ret = new interop.Reference<T>(type, new interop.Pointer());
        invocation.getReturnValue(ret);

        return ret.value;
    }
}

Then use it to read your property in the following way:

const tasksReader = new NativePropertyReader();
const thePropertyValue = tasksReader.readProp(someNativeObject, "thePropertyName", interop.types.int64);

We've used the same approach in background-http plugin, so you can use it as reference. Hope this helps.

@mbektchiev
Copy link
Contributor Author

mbektchiev commented Jul 11, 2019

Yet another case with similar problems was reported by @triniwiz:

device = MTLCreateSystemDefaultDevice();
queue = device.newCommandQueue(); // newCommandQueue undefined :/ 
// apply workaround
// view is MTKView
buffer = queue.commandBuffer(); // undefined
// apply work around again
texture = view.currentDrawable.texture // undefined
// apply work around

buffer.presentDrawable(view.currentDrawable);
/*
 public executeMethod<T>(object: any, method: string, args: any[] , type: interop.Type<T>): T {
        const count = Array.isArray(args) ? args.length : 0;
        const invocation = this.getInvocationObject(object, method, true, count);
        if (count === 1) { // TODO handle more args
            args.forEach((item, index) => {
                invocation.setArgumentAtIndex(new interop.Reference(interop.types.id, item), 2);
            });
        }
        invocation.invokeWithTarget(object);
        const ret = new interop.Reference<T>(type, new interop.Pointer());
        invocation.getReturnValue(ret);
        return ret.value;
    }
*/
buffer.commit();
// apply work around

This second scenario hasn't worked even before the regression of the original issue and is related to #1127.

I'm working on a solution right now and it should handle both cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants