Edit - I've tracked the below issue to a 64-bit vs 32-bit architecture issue... see my posted answer for how I resolved
I've used SudzC to generate SOAP code for a web service. They supply you with a sample application, which I was able to use successfully, both on device and simulator.
I then started building out my app. I imported the SudzC generated files into a new XCode project using the blank application template (with CoreData and ARC enabled).
I got the first SOAP request up and running -- everything works in the simulator -- and then I went to do my first test on a device (iPhone 5S running iOS 7.02). The device throws an EXC_BAD_ACCESS
error every time the SOAP request is run.
I've tracked this down to the SoapRequest.m
file, specifically the connectionDidFinishLoading
method. This method uses a objc_msgSend
call to send the SOAP response data back to a handler method in another class (in this case, my view controller). Here's the code:
SoapRequest.m:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSError* error;
if(self.logging == YES) {
NSString* response = [[NSString alloc] initWithData: self.receivedData encoding: NSUTF8StringEncoding];
NSLog(@"%@", response);
}
CXMLDocument* doc = [[CXMLDocument alloc] initWithData: self.receivedData options: 0 error: &error];
if(doc == nil) {
[self handleError:error];
return;
}
id output = nil;
SoapFault* fault = [SoapFault faultWithXMLDocument: doc];
if([fault hasFault]) {
if(self.action == nil) {
[self handleFault: fault];
} else {
if(self.handler != nil && [self.handler respondsToSelector: self.action]) {
objc_msgSend(self.handler, self.action, fault);
} else {
NSLog(@"SOAP Fault: %@", fault);
}
}
} else {
CXMLNode* element = [[Soap getNode: [doc rootElement] withName: @"Body"] childAtIndex:0];
if(deserializeTo == nil) {
output = [Soap deserialize:element];
} else {
if([deserializeTo respondsToSelector: @selector(initWithNode:)]) {
element = [element childAtIndex:0];
output = [deserializeTo initWithNode: element];
} else {
NSString* value = [[[element childAtIndex:0] childAtIndex:0] stringValue];
output = [Soap convert: value toType: deserializeTo];
}
}
if(self.action == nil) { self.action = @selector(onload:); }
if(self.handler != nil && [self.handler respondsToSelector: self.action]) {
objc_msgSend(self.handler, self.action, output);
} else if(self.defaultHandler != nil && [self.defaultHandler respondsToSelector:@selector(onload:)]) {
[self.defaultHandler onload:output];
}
}
conn = nil;
}
So the line objc_msgSend(self.handler, self.action, output);
seems to be where my issue is. self.handler
is pointing to my View Controller, and self.action
points to this method:
TasksViewController.m:
- (void) findItemHandler: (id) value {
// Handle errors
if([value isKindOfClass:[NSError class]]) {
NSLog(@"%@", value);
return;
}
// Handle faults
if([value isKindOfClass:[SoapFault class]]) {
NSLog(@"%@", value);
return;
}
// Do something with the id result
NSLog(@"FindItem returned the value: %@", value);
}
The re-entry to this method is where I crash out. It looks like the (id)value
is not making it over from the SoapRequest class. I assume it is getting deallocated by ARC. I've tested the call by replacing (id)value
with an int
:
objc_msgSend(self.handler, self.action, 1);
and
- (void)findItemHandler:(int)value
This works. Assuming the issue is the value variable getting prematurely destroyed, I tried a few things to try and keep it retained. I added a property to SoapRequest.m:
@property (nonatomic, strong) id value;
Then passed that:
self.value = output;
objc_msgSend(self.handler, self.action, self.value);
Same problem. I also tried the same thing with the instance of SoapRequest...Now, I was able to work around the issue by creating a property in the view controller and setting that property to the value, but I am really curious how to fix this using the original code. I went back to the example app I downloaded from SudzC to see how that was working, and it turns out ARC is not enabled for that project (!).
Can someone tell me:
1) If I am correct in my assumption that output
is being deallocated, causing the handler method to reference a bad memory address?
2) Why this works on the simulator? I assume it is because the sim has a lot more memory available so it isn't as aggressive with ARC deallocations...
3) How I could fix this, assuming I want to keep the objc_msgSend
call? I want to learn how/why this is happening
4) If SudzC is correct in their usage here of objc_msgSend
, as I understand it is bad practice to call this directly except in rare circumstances?
Thanks!
OK - so after more hair-pulling and research, it finally dawned on me that it might be a 64-bit vs 32-bit issue. This was the first app I was working on since upgrading to the new Xcode. I went to Build Settings and changed the Architectures from "Standard architectures (including 64-bit) (armv7, armv7s, arm64)" to "Standard architectures (armv7, armv7s)". This fixed the issue!
I then went back and researched why this was happening. I found this Apple 64-bit transition guide: https://developer.apple.com/library/content/documentation/General/Conceptual/CocoaTouch64BitGuide/ConvertingYourAppto64-Bit/ConvertingYourAppto64-Bit.html
The document mentions the following:
Dispatch Objective-C Messages Using the Method Function’s Prototype An exception to the casting rule described above is when you are calling the objc_msgSend function or any other similar functions in the Objective-C runtime that send messages. Although the prototype for the message functions has a variadic form, the method function that is called by the Objective-C runtime does not share the same prototype. The Objective-C runtime directly dispatches to the function that implements the method, so the calling conventions are mismatched, as described previously. Therefore you must cast the objc_msgSend function to a prototype that matches the method function being called.
Listing 2-14 shows the proper form for dispatching a message to an object using the low-level message functions. In this example, the doSomething: method takes a single parameter and does not have a variadic form. It casts the objc_msgSend function using the prototype of the method function. Note that a method function always takes an id variable and a selector as its first two parameters. After the objc_msgSend function is cast to a function pointer, the call is dispatched through that same function pointer.
Using this information, I changed the below line:
objc_msgSend(self.handler, self.action, self.value);
to:
id (*response)(id, SEL, id) = (id (*)(id, SEL, id)) objc_msgSend;
response(self.handler, self.action, output);
And all is working!
Hopefully this will help someone else...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With