Search code examples
objective-cmacosobjective-c-blocksxpc

How can I return a value from a block?


I am quite humbled to admit that I have no clue about blocks.

I am writing a GUI uninstaller app that uses a privileged helper tool delete all of my product's files and directories.

Because the helper is a single file, I embed its Info.plist into the executable by creating a TEXT segment then copying Info.plist into it.

So far I can successfully use SMJobBless() to install that helper tool in /Library/PrivilegedHelperTools and its launchd property list in /Library/LaunchDaemons.

Apple recommends using XPC to ensure that the GUI app and the tool have binary-compatible versions. The EvenBetterAuthorizationSample uses a block to call into the helper tool. I need my getVersion function to pass back an NSMutableString to verifyVersionCompatibility.

- (uint32_t)getVersion: (NSMutableString**) helperVersionPtr
    // Called when the user clicks the Get Version button.
    // This is the simplest form of
    // NSXPCConnection request because it doesn't require any authorization.
{
    assert( helperVersionPtr != NULL );
    assert( *helperVersionPtr != NULL );

    [self connectAndExecuteCommandBlock:^(NSError * connectError) {
        if (connectError != nil) {
            [self logError:connectError];
            *helperVersionPtr = NULL;

            DBG_MSG(( "%s connectAndExecuteCommandBlock failed connectError: %s\n", __func__, [[connectError description] UTF8String] ));

        } else {
            [[self.helperToolConnection remoteObjectProxyWithErrorHandler:^(NSError * proxyError) {
                [self logError:proxyError];
            }] getVersionWithReply:^(NSString *version) {

                [self logWithFormat:@"version = %@\n", version];

                [*helperVersionPtr setString: version];  // Pass the version back
            }];
        }
    }];

    return 0;
}

The block gets executed here:

- (void)connectAndExecuteCommandBlock:(void(^)(NSError *))commandBlock
    // Connects to the helper tool and then executes 
    // the supplied command block on the 
    // main thread, passing it an error indicating 
    // if the connection was successful.
{
    assert([NSThread isMainThread]);

    // Ensure that there's a helper tool connection in place.

    [self connectToHelperTool];

    // Run the command block.  
    // Note that we never error in this case because, if there is 
    // an error connecting to the helper tool, it will be delivered 
    // to the error handler 
    // passed to -remoteObjectProxyWithErrorHandler:.
    // However, I maintain the possibility 
    // of an error here to allow for future expansion.

    commandBlock(nil);
}

The helper tool's getVersionWithReply:

- (void)getVersionWithReply:(void(^)(NSString * version))reply
    // Part of the HelperToolProtocol.
    // Returns the version number of the tool.  Note that never
    // requires authorization.
{
    // We specifically don't check for authorization here.
    // Everyone is always allowed to get
    // the version of the helper tool.

    reply([[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]);

}


Solution

  • I think what you're asking is how can -getVersion: synchronously obtain the string value obtained via the XPC reply function and return it to the caller.

    The problem is that all XPC messages/handlers execute asynchronously and on random threads.

    If you really must have a synchronous call, you can use semaphores to block until the reply is received:

    - (NSString*)synchronousExample
    {
        NSConditionLock* barrierLock = [[NSConditionLock alloc] initWithCondition:NO];
        id<YourXPCServiceMessaging> proxy = self.serviceProxy;  // get the proxy object of the XPC connection
    
        // Send the XPC message that requests a response
        __block NSString* replyString = nil;
        [proxy someMessageWithStringReply:^(NSString* string){
            // The XPC service has responded with the value; squirrel it away and let the original thread know it's done
            replyString = string;
            [barrierLock lock];
            [barrierLock unlockWithCondition:YES];
            }];
        // Wait for the reply block to execute
        [barrierLock lockWhenCondition:YES];
        [barrierLock unlock];
    
        return replyString;
    }
    

    A simpler approach, if you can reorganize your code, is to make an asynchronous request and then continue once the reply is received:

    - (void)startVerifyCheck
    {
        id<YourXPCServiceMessaging> proxy = self.serviceProxy;  // get the proxy object of the XPC connection
        [proxy someMessageWithStringReply:^(NSString* string){
            // The XPC service has responded with the value
            if ([string isEqualToString:@"the expected value"])
                {
                // Verify successful: continue with the next step
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self verifyComplete];
                    });
                }
            }];
        // Reply block will continue once the reply is received
    }
    
    - (void)verifyComplete
    {
        // do the next step here...
    }