17

I need to create in an iOS application a fake va_list to pass to a NSString initWithFormat:arguments: function, this is my code:

NSArray *fixedArguments = [[NSArray alloc] initWithArray:arguments]; 

NSRange range = NSMakeRange(0, [fixedArguments count]);

va_list fakeArgList = (va_list)malloc(sizeof(NSString *) * [fixedArguments count]);

__unsafe_unretained id *ptr = (__unsafe_unretained id *)fakeArgList;

[fixedArguments getObjects:ptr range:range];

content = [[NSString alloc] initWithFormat:outputFormat
                                            arguments:(va_list)fakeArgList];
free(fakeArgList);

The compiler complains with this message on the cast line:

error: cast of a non-Objective-C pointer type 'va_list' (aka 'char *') to '__unsafe_unretained id *' is disallowed with ARC

The getObjects:range: function is defined as follows:

- (void)getObjects:(id __unsafe_unretained [])objects range:(NSRange)range;

I've tried everything but still can't get rid of this error...

Is there a solution for creating a fake va_list with ARC enabled? What am i doing wrong?

Scakko
  • 548
  • 6
  • 17

2 Answers2

31

EDIT: This no longer works. As foreseen in the initial answer, the ABI appears to have changed out from under this answer

Played around for a bit and got it to work -- Double checked for leaks or abandoned memory and didn't see any.

    NSArray *fixedArguments = [[NSArray alloc] initWithObjects: @"foo", @"bar", @"baz", nil]; 

    NSRange range = NSMakeRange(0, [fixedArguments count]);

    NSMutableData* data = [NSMutableData dataWithLength: sizeof(id) * [fixedArguments count]];    

    [fixedArguments getObjects: (__unsafe_unretained id *)data.mutableBytes range:range];

    NSString* content = [[NSString alloc] initWithFormat: @"1: %@ 2: %@ 3: %@"  arguments: data.mutableBytes];

    NSLog(@"%@", content);

I like to (ab)use NSMutableData like this to get retain/release semantics on an arbitrary chunk of memory -- It's not necessarily relevant to the issue at hand, but it's a neat little trick.

As a note to future readers: Faking up a va_list like this happens to work with the current ABI for MacOS and iOS, but in general it's not portable, and not a good approach.

ipmcc
  • 29,581
  • 5
  • 84
  • 147
  • Thank you so much... I'm building a picker view that given a plist contained array of dictionaries, a print format, let's say `"(%@ - %@) %@"`, and a list of keys populates the picker view with the formatted string extracting the data from the plist file. The only way i found to use a formatted print with a variable list of arguments was faking a va_list. I know it's far from being clean programming but i couldn't come up with a better solution, any valid alternative is really welcome and i think i will post another question about my problem to find a cleaner solution. – Scakko Nov 22 '11 at 00:13
  • If you're always working with %@ and never any other sized params, you can just search for instances of %@ in the string and replace them with [object description] for each of your parameters. Same effect, no fake va_list. But that won't work for numeric formatting or anything else, unless you wanna do a lot of extra work. – ipmcc Nov 22 '11 at 00:28
  • 1
    Well done! Really nice use of the mutable data to avoid dealing with malloc. Really like that! This solution will be useful for `loc-args` and `loc-key` push notification handling. – MonsieurDart Oct 17 '13 at 15:01
  • About the idea of replacing %@, caution with the localization as the %@ could be replaced by %1@, %2@… in this case. – MonsieurDart Oct 17 '13 at 15:02
  • gives EXC_BAD_ADDRESS type EXC_I386_GPFLT for 64bit architecture :s – Haris Hussain Apr 29 '14 at 08:30
  • Looks like the ABI has changed out from underneath this answer. It's always been a bit of a sore spot for me that this was one of my most up-voted answers. I guess now I can feel vindicated about having warned that this was not a good approach. :) – ipmcc Apr 29 '14 at 09:45
  • 4
    So now this doesn't work under 64-bit architecture, are there any workarounds? – jowie Mar 26 '15 at 12:52
0

Its possible if you are willing to add a little bit of swift to your project!

The important bit is the mapping of NSArray to [CVarArgType] which is the swift equivalent for va_list. If you try to cast [AnyObject] to [CVarArgType] you cause run time crashes, but with the map we can explicitly make the needed list.

The rest of the code is the wrapper I made so that I can call this from obj-c. You could make a wrapper for any obj-c function that you want to call in this way.

@objc class StringFormat: NSObject {
    class func format(key: String, args: [AnyObject]) -> String {
        let locArgs: [CVarArgType] = args.map({ (arg: AnyObject) -> CVarArgType in
            if let iArg = (arg is NSNumber ? arg.intValue : nil) {
                return iArg
            }
            return arg as! CVarArgType
        });
        return String(format: key, arguments: locArgs)
    }
}
mcfedr
  • 7,845
  • 3
  • 31
  • 27