How to pass a JS function as object property to iOS - ios

I have a JSExport protocol named ChannelExport with the method - (void)sendRequest:(NSDictionary *)request withCallback:(JSValue *)callback;. Calling this method from Javascript code works alright, like this:
channel.sendRequestWithCallback({'foo': 'bar'}, function(response) { ... });
In ObjC, I can access the values in the request dictionary and also call the callback function.
Now I'd like to change the interface to - (void)sendRequest:(NSDictionary *)request, passing the JS function as part of the request dictionary, like this:
channel.sendRequestWithCallback({
'foo': 'bar'
'callback': function(response) { ... }
});
In this case, when I try to call the callback function in ObjC, the app crashes. Apparently the callback object is not a JSValue, but instead an NSDictionary (to be more precise, an __NSDictionaryM). I was assuming that the JS function is correctly wrapped as JSValue in the same way as it is when passing it as a simple parameter.
Any hint why this is happening, and how to solve the issue?

You can't use - (void)sendRequest:(NSDictionary *)request signature to achieve your goal. If you define argument as NSDictionary, JavaScriptCore will recursively convert all objects in this dictionary to corresponding Objective-C objects. Use - (void)sendRequest:(JSValue *)requestValue instead:
- (void)sendRequest:(JSValue *)requestValue {
JSValue *fooJSValue = [requestValue valueForProperty:#"foo"];
NSString *bar = [fooJSValue isUndefined] ? nil : fooValue.toString;
// use bar
JSValue *callback = [requestValue valueForProperty:#"callback"];
[callback callWithArguments:myArguments];
}

Related

Send closure from Objective-C to JavaScript in iOS React-Native

Is it possible to construct a closure in objective-c and pass it to javascript where it can be invoked? The specific problem I am trying to solve is adding support for changing shipping methods and contacts in Apple Pay as part of the tipsi-stripe react-native module (something it doesn't do yet). This is basically what I have so far, but the callback in javascript gets 'null'.
- (void) paymentAuthorizationViewController:(PKPaymentAuthorizationViewController *)controller
didSelectShippingMethod: (PKShippingMethod *) shippingMethod
completion:(nonnull void (^)(PKPaymentAuthorizationStatus, NSArray<PKPaymentSummaryItem *> * _Nonnull))completion {
id callback = (void (^)(NSArray* summaryItems)) {
completion(PKPaymentAuthorizationStatusSuccess, nil, summaryItems);
}
[self sendEventWithName: "#ShippingMethodChanged" body:#{#"selectedMethod": #"someMethodDetails", #"callback": callback}];
}
In javascript, I have something like this:
import { NativeEventEmitter, NativeModules } from 'react-native'
const { TPSStripeManager } = NativeModules;
const stripeEventEmitter = new NativeEventEmitter(TPSStripeManager);
componentWillMount() {
this.stripeOnShippingMethodChanged = stripeEventEmitter.addListener(
'ShippingMethodChanged',
(method, callback) => {
// async compute some value then
let summaryItems = await computeItemsWithMethod(method);
callback(summaryItems);
}
);
}
componentWillUnmount() {
this.stripeOnShippingMethodChanged.remove();
}
I'm assuming I have to somehow wrap the Objective-C closure so javascript knows how to invoke it but I can't find anything. Any help appreciated!
It is not possible to do what I was attempting to do here, the react-native bindings only support simple types that can be encoded as a string. The solution that I ended up with based on patterns I found elsewhere is to store the completion as a property on the object, trigger an event that can be seen in js-land and provide another exported method that can be called to trigger the completion. Code is here:
https://github.com/tipsi/tipsi-stripe/pull/244/files

Add C function as value to NSDictionary

Upon receiving an NSString, I would like to call a specific code block. I figured an NSDictionary would be best for associating these. Simplified, I'm using something like:
MyProtocol.h:
#protocol MyProtocol <NSObject>
typedef void (^Handler)(id<MyProtocol> obj, id data);
#end
MyClass.h:
#interface MyClass : NSObject <MyProtocol>
- (void)aMethodWithString:(NSString *)string andData:(id)data;
#end
MyClass.m:
#interface MyClass ()
void myCommandHandler(id<MyProtocol> obj, id data); // matches signature defined in protocol
#end
#implementation MyClass
void myCommandHandler(id<MyProtocol> obj, id data)
{
// ...
}
- (void)aMethodWithString:(NSString *)string andData:(id)data
{
static NSDictionary<NSString *, Handler> *handler;
static dispatch_once_t onceToken;
// don't allocate this dictionary every time the function is called
dispatch_once(&onceToken,
^{
handler =
#{
#"MyCommand":myCommandHandler,
};
});
// ... error checking, blah blah ...
Handler block;
if ((block = handler[string]))
{ block(self, data); }
}
#end
Using this, I get an error in the dictionary literal construction:
Collection element of type 'void (__strong id<MyProtocol>, __strong id)' is not an Objective-C object`
So how can I include a C function or block reference in the dictionary? There will be quite a few larger complex functions to be defined, so it would be very much preferred to not have all of them defined inside the dictionary literal itself (a technique I know will work).
--
Also, I'm not sure what's considered proper style here: (1) I originally had the dictionary declaration outside of any method body at the file scope (without the dispatch_once(...)) which generates the same error, but I thought maybe it would be easier for others to see what's going on by (2) including it in the only method that uses that dictionary. Is one style preferred over the other for any reason?
Your Handler type is a block type, not a function pointer type. If you had declared it using * instead of ^, it would be a function pointer type.
Your myCommandHandler function is, of course, a function, not a block. Your comment is wrong. It does not match type Handler.
Function pointers are not Objective-C object pointers. Functions are not objects. Blocks are Objective-C objects, but you're not actually using any blocks here. (You've just declared a typedef for one, but you're not actually using it.)
You could use blocks and store them in your dictionary. The blocks could either contain the desired code or call a function or method which does.
A C function address is not an Objective-C object, and an NSDictionary can only store the latter.
A C function address is just a pointer, to wrap a C pointer as an object you use NSValue and its class method valueWithPointer.
To get the pointer value back from the NSValue object you use the instance method pointerValue. Before you can use the extracted pointer you must cast it to your function type.
HTH

Send javascript function to objective-C using JavascriptCore

I'm trying to send a Javascript function object to Objective-C via JavascriptCore, leveraging the JSExport protocol. I have a function declared in Objective-C, conforming to JSExport as follows:
(class View)
+ (void) newWithFunc:(id)func
{
NSLog(#" %# ", func);
}
After declaring this class, I try to call the function above with a Javascript function object as a parameter
JSValue *val;
val = [context evaluateScript:#"var mufunc = function() { self.value = 10; };"];
val = [context evaluateScript:#"mufunc;"];
NSLog(#" %#", val); //Prints as 'function() { self.value = 10; }', seems correct.
val = [context evaluateScript:#"var view = View.newWithFunc(mufunc);"];
When the last call is made, the parameter sent to my Objective-C method is of type 'NSDictionary', which doesn't seem very valuable if what I would like to do is call that function from Objective-C at a later point in time. Is this possible with JavascriptCore?
Please mark Tayschrenn's answer as correct. I don't know how he knew this or where it's documented, but this is what I figured out by trial and error:
- (void)newWithFunc: (JSValue*)func
{
[func callWithArguments:#[]]; // will invoke js func with no params
}
Declaring the parameter (id)func apparently causes the javascript-cocoa bridge to convert it to an NSDictionary (as you noticed), rendering it unusable as a callable JSValue.
This is definitely possible, but you need to change newWithFunc: to accept a JSValue* rather than plain id. The reason is that JSValue* is a special type for JavaScriptCore - it won't try to convert the JS value to its native equivalent but rather wrap it in a JSValue and pass it on.

Use an NSString to programatically access or create a method in Objective-C

I am trying to use an array of strings dynamically access methods at runtime within my class. For now the methods are already there, eventually I want to create them.
Is this possible?
For example:
bool nextLevel=NO;
for(NSString * match in gameLevels)
{
if([match isEqualToString:self.level])
{
nextLevel=YES;
}
else if(nextLevel==YES)
{
self.level=match;
nextLevel=NO;
}
}
//access method named self.level
Thank you in advance!
I use:
NSSelectorFromString(selectorString)
In your case, the selectorString would be:
NSString * selectorString = #"setLevel:";
This is 'setLevel' instead of 'level' because the Objective-C runtime will automatically expand dot properties to these selector names when assignment occurs.
To access a method based on a string, check the other answer.
To add a method in the runtime you need to create a IMP function or block.
If using a function, could be something like:
void myMethodIMP(id self, SEL _cmd)
{
// implementation ....
}
You could also use a block like this:
IMP blockImplementation=imp_implementationWithBlock(^(id _self, ...){
//Your Code here
}
Then you need to add the method, like this:
class_addMethod(yourClass, #selector(selectorName), (IMP) blockImplementation, encoding);
The encoding part is a special runtime encoding to describe the type of parameters your method receives. You can find that on the Objective-C runtime reference.
If you receive dynamic arguments on your generated methods, you need to use the va_list to read the values.

How to pass objective-c function as a callback to C function?

I want to call a c function from objective-c and pass objective-c function as a callback
the problem is this function has a callback as parameter, so I have to pass objective-c function as a call back to c function
here is the header of the c function
struct mg_context *mg_start(const struct mg_callbacks *callbacks,
void *user_data,
const char **configuration_options);
here is where I try to call it
- (void)serverstarted
{
NSLog(#"server started");
}
- (IBAction)startserver:(id)sender {
NSLog(#"server should start");
const char *options[] =
{
"document_root", "www",
"listening_ports", "8080",
NULL
};
mg_start(serverstarted(), NULL, options);
}
I have tried several ways to do it and searched the web to just get a clue how to do it but with not luck
here is the library I am incuding in my code
https://github.com/valenok/mongoose
Your chief problem is the first parameter to mg_start(), which is described in the declaration as const struct mg_callbacks *callbacks. You are trying pass a pointer to a function. (Actually you are trying to pass the result of a call to that function, which is even further from the mark.) That isn't what it says: it says a pointer to a struct (in particular, an mg_callbacks struct).
The example code at https://github.com/valenok/mongoose/blob/master/examples/hello.c shows you how to configure this struct. You have to create the struct and put the pointer to the callback function inside it. Then you pass the address of that struct.
Other problems with your code: your callback function itself is all wrong:
- (void)serverstarted
{
NSLog(#"server started");
}
What's wanted here is a C function declared like this: int begin_request_handler(struct mg_connection *conn), that is, it takes as parameter a pointer to an mg_connection struct. Your serverstarted not only doesn't take that parameter, it isn't even a C function! It's an Objective-C method, a totally different animal. Your use of the term "Objective-C function" in your title and your question is misleading; C has functions, Objective-C has methods. No Objective-C is going to be used in the code you'll be writing here.
What I suggest you do here is to copy the hello.c example slavishly at first. Then modify the content / names of things slowly and bit by bit to evolve it to your own code. Of course learning C would also help, but you can probably get by just by copying carefully.
As matt already said, you cannot pass an Objective-C method as callback where a C function
is expected. Objective-C methods are special functions, in particular the receiver ("self")
is implicitly passed as first argument to the function.
Therefore, to use an Objective-C method as request handler, you need an (intermediate) C function as handler and you have to pass self to that function, using the user_data argument. The C function can then call the Objective-C method:
// This is the Objective-C request handler method:
- (int)beginRequest:(struct mg_connection *)conn
{
// Your request handler ...
return 1;
}
// This is the intermediate C function:
static int begin_request_handler(struct mg_connection *conn) {
const struct mg_request_info *request_info = mg_get_request_info(conn);
// Cast the "user_data" back to an instance pointer of your class:
YourClass *mySelf = (__bridge YourClass *)request_info->user_data;
// Call instance method:
return [mySelf beginRequest:conn];
}
- (IBAction)startserver:(id)sender
{
struct mg_callbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.begin_request = begin_request_handler;
const char *options[] =
{
"document_root", "www",
"listening_ports", "8080",
NULL
};
// Pass "self" as "user_data" argument:
mg_start(&callbacks, (__bridge void *)self, options);
}
Remarks:
If you don't use ARC (automatic reference counting) then you can omit the (__bridge ...)
casts.
You must ensure that the instance of your class ("self")
is not deallocated while the server is running. Otherwise the YourClass *mySelf
would be invalid when the request handler is called.

Resources