How to Invoke Method with Cvalistpointer Parameters in Swift

How to invoke method with CVaListPointer parameters in Swift

CVaListPointer is the Swift equivalent of the C va_list type
and can be created from an [CVarArgType] array using withVaList().

Example:

func log(format: String!, withParameters valist: CVaListPointer) {
NSLogv(format, valist)
}

let args: [CVarArgType] = [ "foo", 12, 34.56 ]
withVaList(args) { log("%@ %ld %f", withParameters: $0) }

Output:


2016-04-27 21:02:54.364 prog[6125:2476685] foo 12 34.560000

For Swift 3, replace CVarArgType by CVarArg.

Swift function with args... pass to another function with args

Similar as in (Objective-)C, you cannot pass a variable argument list
directly to another function. You have to create a CVaListPointer
(the Swift equivalent of va_list in C) and call a function which
takes a CVaListPointer parameter.

So this could be what you are looking for:

extension String {
func getLocalizeWithParams(args : CVarArgType...) -> String {
return withVaList(args) {
NSString(format: self, locale: NSLocale.currentLocale(), arguments: $0)
} as String
}
}

withVaList() creates a CVaListPointer from the given argument list
and calls the closure with this pointer as argument.

Example (from the NSString documentation):

let msg = "%@:  %f\n".getLocalizeWithParams("Cost", 1234.56)
print(msg)

Output for US locale:

Cost:  1,234.560000

Output for German locale:

Cost:  1.234,560000

Update: As of Swift 3/4/5 one can pass the arguments to

String(format: String, locale: Locale?, arguments: [CVarArg])

directly:

extension String {
func getLocalizeWithParams(_ args : CVarArg...) -> String {
return String(format: self, locale: .current, arguments: args)
}
}

Variadic method in Swift

How about just

convenience init(args: Int...) {
return args.count
}

How to properly use VarArgs for localizing strings?

You cannot pass a variable argument list to another function, you
have to pass a CVaListPointer instead (the Swift equivalent
of va_list in C):

public extension String {
var localized: String {
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
}

func localized(args: CVarArg...) -> String {
return withVaList(args) {
NSString(format: self.localized, locale: Locale.current, arguments: $0) as String
}
}
}

Since NSString.localizedStringWithFormat has no variant taking a
VAListPointer, the equivalent NSString(format:, locale:, arguments:)
with the current locale is used.

Even simpler (attribution goes to @OOPer): Use
String.init(format:locale:arguments:) which takes a
[CVarArg] argument:

    func localized(args: CVarArg...) -> String {
return String(format: self.localized, locale: Locale.current, arguments: args)
}

Now

"grant_gps_access".localized(args: "MyApp")

should work as expected, assuming that the strings file contains
the entry

"grant_gps_access" =  "Please grant %@ GPS access";

What are the difference and use-cases for va_list, CVaListPointer, AnyObject ..., CVarArgType?

va_list is a C type used for variable argument functions. You'll see this as a parameter in Objective-C code.

CVaListPointer is the swift equivalent of a va_list--wherever you see a function in Objective-C that takes va_list as an argument, you'll pass in a CVaListPointer in Swift.

objective-c: (NSPredicate *)predicateWithFormat:(NSString *)format arguments:(va_list)argList
swift: init(format format: String, arguments argList: CVaListPointer) -> NSPredicate

CVarArgType is a protocol that defines what kind of types can be included in a CVaListPointer, this includes all the primitives (Int, Float, UInt, etc) as well as COpaquePointer

The utility function withVaList takes a Swift array and transforms it into a CValListPointer, which is then passed to a callback. Note that the array passed in must contain only CVarArgType variables:

var format = "%@ != %@"
var args: [CVarArgType] = ["abc", "def"]

var s = withVaList(args) { (pointer: CVaListPointer) -> NSPredicate in
return NSPredicate(format: format, arguments: pointer)
}

Swift also defines its own varadic parameter T..., which must be the last parameter in a function, it is passed into the function as [T] and is used like so.

var arg1: String = "abc"
var arg2: String = "def"
NSPredicate(format: format, arg1, arg2)

Use Swift's built in varadic parameters T... wherever possible. CValListPointer should only be used when you need to interface with Objective C and C APIs that accept va_list arguments.

Is Swift's handling of CVarArg for String buggy?

This one's a bit tricky: your string is actually being bridged to an Objective-C NSString * rather than a C char *:

(lldb) p str
(const char *) $0 = 0x3cbe9f4c5d32b745 ""
(lldb) p (id)str
(NSTaggedPointerString *) $1 = 0x3cbe9f4c5d32b745 @"Test"

(If you're wondering why it's an NSTaggedPointerString rather than just an NSString, this article is a great read -- in short, the string is short enough to be stored directly in the bytes of the pointer variable rather than in an object on the heap.

Looking at the source code for withVaList, we see that a type's va_list representation is determined by its implementation of the _cVarArgEncoding property of the CVarArg protocol. The standard library has some implementations of this protocol for some basic integer and pointer types, but there's nothing for String here. So who's converting our string to an NSString?

Searching around the Swift repo on GitHub, we find that Foundation is the culprit:

//===----------------------------------------------------------------------===//
// CVarArg for bridged types
//===----------------------------------------------------------------------===//
extension CVarArg where Self: _ObjectiveCBridgeable {
/// Default implementation for bridgeable types.
public var _cVarArgEncoding: [Int] {
let object = self._bridgeToObjectiveC()
_autorelease(object)
return _encodeBitsAsWords(object)
}
}

In plain English: any object which can be bridged to Objective-C is encoded as a vararg by converting to an Objective-C object and encoding a pointer to that object. C varargs are not type-safe, so your test_va_arg_str just assumes it's a char* and passes it to puts, which crashes.

So is this a bug? I don't think so -- I suppose this behavior is probably intentional for compatibility with functions like NSLog that are more commonly used with Objective-C objects than C ones. However, it's certainly a surprising pitfall, and it's probably one of the reasons why Swift doesn't like to let you call C variadic functions.


You'll want to work around this by manually converting your strings to C-strings. This can get a bit ugly if you have an array of strings that you want to convert without making unnecessary copies, but here's a function that should be able to do it.

extension Collection where Element == String {
/// Converts an array of strings to an array of C strings, without copying.
func withCStrings<R>(_ body: ([UnsafePointer<CChar>]) throws -> R) rethrows -> R {
return try withCStrings(head: [], body: body)
}

// Recursively call withCString on each of the strings.
private func withCStrings<R>(head: [UnsafePointer<CChar>],
body: ([UnsafePointer<CChar>]) throws -> R) rethrows -> R {
if let next = self.first {
// Get a C string, add it to the result array, and recurse on the remainder of the collection
return try next.withCString { cString in
var head = head
head.append(cString)
return try dropFirst().withCStrings(head: head, body: body)
}
} else {
// Base case: no more strings; call the body closure with the array we've built
return try body(head)
}
}
}

func withVaListOfCStrings<R>(_ args: [String], body: (CVaListPointer) -> R) -> R {
return args.withCStrings { cStrings in
withVaList(cStrings, body)
}
}

let argsStr: [String] = ["Test", "Testing", "The test"]
withVaListOfCStrings(argsStr) { listPtr in
test_va_arg_str(Int32(argsStr.count), listPtr)
}

// Output:
// Printing 3 strings...
// Test
// Testing
// The test

localizeWithFormat and variadic arguments in Swift

This should be pretty simple just change your parameters as follow:

extension String {
func localizeWithFormat(name:String,age:Int, comment:String = "") -> String {
return String.localizedStringWithFormat( NSLocalizedString(self, comment: comment), name, age)
}
}

"My name is %@. I am %d years old".localizeWithFormat("John", age: 30) // "My name is John. I am 30 years old"

init(format:locale:arguments:)

extension String {
func localizeWithFormat(args: CVarArgType...) -> String {
return String(format: self, locale: nil, arguments: args)
}
func localizeWithFormat(local:NSLocale?, args: CVarArgType...) -> String {
return String(format: self, locale: local, arguments: args)
}
}
let myTest1 = "My name is %@. I am %d years old".localizeWithFormat(NSLocale.currentLocale(), args: "John",30)
let myTest2 = "My name is %@. I am %d years old".localizeWithFormat("John",30)

how to transfer objective-c language (`va_list`,`va_start `,`va_end ` ) to swift language?

From @MartinR comment and reference to NSLog is unavailable you know that Swift can call (Objective-)C functions & methods that take va_list arguments through the use of the Swift types CVarArg and CVaListPointer.

Many of the common (Objective-)C variadic functions & methods have a sibling which takes a va_list, so this support in Swift provides access to them.

I also need to call this swift method in objective-c xxx.m file.

However you wish to go the other way, and having written a Swift variadic function version of your Objective-C method you found you couldn't call it. You attempted to ask what the solution was, How do you call a Swift variadic method from Objective-C?, the indirect answer (your question was marked as duplicate) to that question provides a hint – to use an array – but doesn't handle the generality you require for your formatted-print type scenario. Let's see if we can get there...

(Using Xcode 10/Swift 4.2, any other version of Swift is probably different.)

We'll use the following Swift class as basis:

class SwiftLog : NSObject
{
// Swift entry point
static func Log(_ format : String, args : CVarArg...)
{
withVaList(args) { LogV(format, $0)}
}

// Shared core
private static func LogV(_ format : String, _ args: CVaListPointer)
{
NSLogv(format, args)
}
}

This provides Swift with a variadic function which will take all the Swift library types you are probably interested in, and a few more you not (3287 are listed in Apple's CVarArg documentation). The private core function here is trivial, you probably wish to do something a little more involved.

Now you wish to call Log() from Objective-C but, as you've discovered, you cannot due to the CVarArg. However Objective-C can call Swift functions which take NSObject arguments, and NSObject implements CVarArg, which gets us to our first attempt:

   // Objective-C entry point
@objc static func Log(_ format : String, args : [NSObject])
{
withVaList(args) { LogV(format, $0) }
}

This works as-is but every argument must be an object and formatted with %@, switching to Objective-C:

[SwiftLog LogObjects:@"%@|%@|%@|%@|%@|%@" args:@[@"42", @4.2, @"hello", @31, @'c', NSDate.new]];

produces:

42|4.2|hello|31|99|Sun Nov 11 08:47:35 2018

It works within limits, we have lost the flexibility of formatting – no %6.2f, %x etc. – and the character has come out as 99.

Can we improve it? If you are prepared to sacrifice the ability to print NSNumber values as is, then yes. Over in Swift change the Log() function to:

   @objc static func Log(_ format : String, args : [NSObject])
{
withVaList(args.map(toPrintfArg)) { LogV(format, $0) }
}

Skipping toPrintfArg for the moment (its just large and ugly) over in Objective-C we can call this version as:

[SwiftLog Log:@"%@|%4.2f|%10s|%x|%c|%@" args:@[@"42", @4.2, @((intptr_t)"hello"), @31, @'c', NSDate.new]];

which produces:

42|4.20|     hello|1f|c|Sun Nov 11 08:47:35 2018

Much better, and the character is correct. So what does toPrintfArg do?

In the above we had to pass an array of objects to Swift, and to do that all the primitive values are wrapped as NSNumber objects.

In Objective-C an NSNumber object does not reveal much about what it wraps, the access methods (.doubleValue, .integerValue etc.) will convert whatever the wrapped value was into a value of the requested type and return it.

However NSNumber is "toll-free bridged" to the the Core Foundation types CFBoolean and CFNumber; the former of these is for booleans (obviously!) and the latter for all the other numeric types and, unlike NSNumber, provides a function that returns the type of the wrapped value so it can be unwrapped without conversion. Using this information we can extract the original (experts, yes, see below) values from the NSNumber objects, all those extracted value in Swift will all implement CVarArg, here goes:

   private static func toPrintfArg(_ item : NSObject) -> CVarArg
{
if let anumber = item as? NSNumber
{
if type(of:anumber) == CFBoolean.self { return anumber.boolValue }

switch CFNumberGetType(anumber)
{
case CFNumberType.sInt8Type: return anumber.int8Value
case CFNumberType.sInt16Type: return anumber.int16Value
case CFNumberType.sInt32Type: return anumber.int32Value
case CFNumberType.sInt64Type: return anumber.int64Value
case CFNumberType.float32Type: return Float32(anumber.floatValue)
case CFNumberType.float64Type: return Float64(anumber.doubleValue)
case CFNumberType.charType: return CChar(anumber.int8Value)
case CFNumberType.shortType: return CShort(anumber.int16Value)
case CFNumberType.intType: return CInt(anumber.int32Value)
case CFNumberType.longType: return CLong(anumber.int64Value)
case CFNumberType.longLongType: return CLongLong(anumber.int64Value)
case CFNumberType.floatType: return anumber.floatValue
case CFNumberType.doubleType: return anumber.doubleValue
case CFNumberType.cfIndexType: return CFIndex(anumber.int64Value)
case CFNumberType.nsIntegerType: return NSInteger(anumber.int64Value)
case CFNumberType.cgFloatType: return CGFloat(anumber.doubleValue)
}
}

return item;
}

This function will unwrap (experts, yes, most, see below) NSNumber objects to the original value type while leaving all other objects as is to be formatted by %@ (as shown by the NSString and NSDate objects in the example).

Hope that helps, at least more than it confuses! Notes for the curious/experts follow.


Notes & Caveats

Preserving C Pointers

In the above example the C string "hello" was passed by converting it to intptr_t, a C integer type the same size as a pointer, rather than as a pointer value. In this context this is fine, a va_list is essentially an untyped bob of bytes and the format tells NSLogv() what type to interpret the next bytes as, converting to intptr_t keeps the same bytes/bits and allows the pointer to be wrapped as an NSNumber.

However if you application needs to have an actual pointer on the Swift side you can instead wrap the C string as an NSValue:

[NSValue valueWithPointer:"hello"]

and unwrap it in toPrintfArg by adding:

      if let ptr = (item as? NSValue)?.pointerValue
{
return ptr.bindMemory(to: Int8.self, capacity: 1)
}

This produces a value of type UnsafeMutablePointer<Int8>, which implements CVarArg (and as the latter the capacity is irrelevant).

Do You Always Get The Same Type Back?

If you wrap a C type as an NSNumber and then unwrap it as above, could the type change due to argument promotion (which means that integer types smaller than int get passed as int values, float values as double) in C? The answer is maybe but the required type for the CVarArg value is the promoted type so it should not make any difference in this context – the type of the unwrapped value with suit the expected format specifier.

What about NSDecimalNumber?

Well spotted, if you try to print an NSDecimalNumber, which is a subclass of NSNumber, the above toPrintfArg will unpack it as a double and you must use a floating-point format and not %@. Handling this is left as an exercise.



Related Topics



Leave a reply



Submit