Consider the following example:
import Foundation
import os.log
class OSLogWrapper {
func logDefault(_ message: StaticString, _ args: CVarArg...) {
os_log(message, type: .default, args)
}
func testWrapper() {
logDefault("WTF: %f", 1.2345)
}
}
If I create a new instance of OSLogWrapper
and call testWrapper()
let logger = OSLogWrapper()
logger.testWrapper()
I get the following output in the Xcode console:
2018-06-19 18:21:08.327979-0400 WrapperWTF[50240:548958] WTF: 0.000000
I've checked everything I can think of and I can't make heads or tails of what's going wrong here. Looking through the documentation isn't yielding anything helpful.
The compiler implements variadic arguments by casting each argument to the declared variadic type, packaging them into an
Array
of that type, and passing that array to the variadic function. In the case oftestWrapper
, the declared variadic type isCVarArg
, so whentestWrapper
callslogDefault
, this is what happens under the covers:testWrapper
casts1.2345
to aCVarArg
, creates anArray<CVarArg>
, and passes it tologDefault
asargs
.Then
logDefault
callsos_log
, passing it thatArray<CVarArg>
as an argument. This is the bug in your code. The bug is quite subtle. The problem is thatos_log
doesn't take anArray<CVarArg>
argument;os_log
is itself variadic overCVarArg
. So Swift castsargs
(anArray<CVarArg>
) toCVarArg
, and sticks that castedCVarArg
into anotherArray<CVarArg>
. The structure looks like this:Then
logDefault
passes this newArray<CVarArg>
toos_log
. So you're askingos_log
to format its first element, which is (sort of) anArray<CVarArg>
, using%f
, which is nonsense, and you happen to get0.000000
as output. (I say “sort of” because there are some subtleties here which I explain later.)So,
logDefault
passes its incomingArray<CVarArg>
as one of potentially many variadic parameters toos_log
, but what you actually wantlogDefault
to do is pass on that incomingArray<CVarArg>
as the entire set of variadic parameters toos_log
, without re-wrapping it. This is sometimes called “argument splatting” in other languages.Sadly for you, Swift doesn't yet have any syntax for argument splatting. It's been discussed more than once in Swift-Evolution (in this thread, for example), but there's not yet a solution on the horizon.
The usual solution to this problem is to look for a companion function that takes the already-bundled-up variadic arguments as a single argument. Often the companion has a
v
added to the function name. Examples:printf
(variadic) andvprintf
(takes ava_list
, C's equivalent ofArray<CVarArg>
)NSLog
(variadic) andNSLogv
(takes ava_list
)-[NSString initWithFormat:]
(variadic) and-[NSString WithFormat:arguments:]
(takes ava_list
)So you might go looking for an
os_logv
. Sadly, you won't find one. There is no documented companion toos_log
that takes pre-bundled arguments.You have two options at this point:
Give up on wrapping
os_log
in your own variadic wrapper, because there is simply no good way to do it, orTake Kamran's advice (in his comment on your question) and use
%@
instead of%f
. But note that you can only have a single%@
(and no other format specifiers) in your message string, because you're only passing a single argument toos_log
. The output looks like this:You could also file an enhancement request radar at https://bugreport.apple.com asking for an
os_logv
function, but you shouldn't expect it to be implemented any time soon.So that's it. Do one of those two things, maybe file a radar, and move on with your life. Seriously. Stop reading here. There's nothing good after this line.
Okay, you kept reading. Let's peek under the hood of
os_log
. It turns out the implementation of the Swiftos_log
function is part of the public Swift source code:So it turns out there is a version of
os_log
, called_swift_os_log
, that takes pre-bundled arguments. The Swift wrapper useswithVaList
(which is documented) to convert theArray<CVarArg>
to ava_list
and passes that on to_swift_os_log
, which is itself also part of the public Swift source code. I won't bother quoting its code here because it's long and we don't actually need to look at it.Anyway, even though it's not documented, we can actually call
_swift_os_log
. We can basically copy the source code ofos_log
and turn it into yourlogDefault
function:And it works. Test code:
Output:
Would I recommend this solution? No. Hell no. The internals of
os_log
are an implementation detail and likely to change in future versions of Swift. So don't rely on them like this. But it's interesting to look under the covers anyway.One last thing. Why doesn't the compiler complain about converting
Array<CVarArg>
toCVarArg
? And why does Kamran's suggestion (of using%@
) work?It turns out these questions have the same answer: it's because
Array
is “bridgeable” to an Objective-C object. Specifically:Foundation (on Apple platforms) makes
Array
conform to the_ObjectiveCBridgeable
protocol. It implements bridging ofArray
to Objective-C by returning anNSArray
.Foundation also makes
Array
conform to theCVarArg
protocol.The
withVaList
function asks eachCVarArg
to convert itself to its_cVarArgEncoding
.The default implementation of
_cVarArgEncoding
, for a type that conforms to both_ObjectiveCBridgeable
andCVarArg
, returns the bridging Objective-C object.The conformance of
Array
toCVarArg
means the compiler won't complain about (silently) converting anArray<CVarArg>
to aCVarArg
and sticking it into anotherArray<CVarArg>
.This silent conversion is probably often an error (as it was in your case), so it would be reasonable for the compiler to warn about it, and allow you to silence the warning with an explicit cast (e.g.
args as CVarArg
). You could file a bug report at https://bugs.swift.org if you want.As mentioned in my comment to Rob Mayoff's answer above, for anybody experiencing the same kind of issue with
os_signpost()
, here is a wrapper class I made around it: