为什么包装os_log()导致double不能正确记录? [英] Why does wrapping os_log() cause doubles to not be logged correctly?
问题描述
请考虑以下示例:
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)
}
}
如果我创建OSLogWrapper
的新实例并调用testWrapper()
If I create a new instance of OSLogWrapper
and call testWrapper()
let logger = OSLogWrapper()
logger.testWrapper()
我在Xcode控制台中得到以下输出:
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.
推荐答案
编译器通过将每个参数强制转换为声明的可变参数类型,并将其包装到该类型的Array
中,并将该数组传递给可变参数来实现可变参数功能.对于testWrapper
,声明的可变参数类型为CVarArg
,因此,当testWrapper
调用logDefault
时,情况就是这样:testWrapper
将1.2345
强制转换为CVarArg
,创建一个Array<CVarArg>
,并将其作为args
传递给logDefault
.
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 of testWrapper
, the declared variadic type is CVarArg
, so when testWrapper
calls logDefault
, this is what happens under the covers: testWrapper
casts 1.2345
to a CVarArg
, creates an Array<CVarArg>
, and passes it to logDefault
as args
.
然后logDefault
调用os_log
,并将Array<CVarArg>
作为参数传递给它. 这是您代码中的错误.该错误非常微妙.问题在于os_log
不接受Array<CVarArg>
自变量. os_log
本身对CVarArg
而言是可变的.因此,Swift将args
(一个Array<CVarArg>
)强制转换为CVarArg
,并将将CVarArg
强制转换为另一个 Array<CVarArg>
.结构如下:
Then logDefault
calls os_log
, passing it that Array<CVarArg>
as an argument. This is the bug in your code. The bug is quite subtle. The problem is that os_log
doesn't take an Array<CVarArg>
argument; os_log
is itself variadic over CVarArg
. So Swift casts args
(an Array<CVarArg>
) to CVarArg
, and sticks that casted CVarArg
into another Array<CVarArg>
. The structure looks like this:
Array<CVarArg> created in `logDefault`
|
+--> CVarArg (element at index 0)
|
+--> Array<CVarArg> (created in `testWrapper`)
|
+--> CVarArg (element at index 0)
|
+--> 1.2345 (a Double)
然后logDefault
将此新的Array<CVarArg>
传递给os_log
.因此,您要使用%f
来格式化os_log
的第一个元素(是Array<CVarArg>
的某种),这是毫无意义的,而您恰巧得到了0.000000
作为输出. (我说有点"是因为这里有些微妙之处,我稍后会解释.)
Then logDefault
passes this new Array<CVarArg>
to os_log
. So you're asking os_log
to format its first element, which is (sort of) an Array<CVarArg>
, using %f
, which is nonsense, and you happen to get 0.000000
as output. (I say "sort of" because there are some subtleties here which I explain later.)
因此,logDefault
将其传入的Array<CVarArg>
作为可能的各种可变参数之一传递给os_log
,但是您真正想要的logDefault
要做的是将该传入的Array<CVarArg>
作为整个可变参数传递参数设置为os_log
,而无需重新包装.在其他语言中,有时也称为参数展开".
So, logDefault
passes its incoming Array<CVarArg>
as one of potentially many variadic parameters to os_log
, but what you actually want logDefault
to do is pass on that incoming Array<CVarArg>
as the entire set of variadic parameters to os_log
, without re-wrapping it. This is sometimes called "argument splatting" in other languages.
对您来说很不幸,Swift尚无任何用于参数展开的语法.在Swift-Evolution(例如),但目前还没有解决方案.
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.
此问题的通常解决方案是寻找一个伴随函数,该函数将已经捆绑的可变参数作为单个参数.通常,同伴在函数名称中添加了v
.例子:
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
(可变)和vprintf
(采用va_list
,C等效于Array<CVarArg>
) -
NSLog
(可变)和NSLogv
(采用va_list
) -
-[NSString initWithFormat:]
(可变)和-[NSString WithFormat:arguments:]
(采用va_list
)
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
)
所以您可能会寻找os_logv
.可悲的是,您找不到一个. os_log
没有附带预捆绑参数的伴侣.
So you might go looking for an os_logv
. Sadly, you won't find one. There is no documented companion to os_log
that takes pre-bundled arguments.
这时您有两个选择:
-
放弃使用自己的可变参数包装器包装
os_log
,因为根本没有什么好办法,或者
Give up on wrapping
os_log
in your own variadic wrapper, because there is simply no good way to do it, or
接受Kamran的建议(在对您的问题的评论中),并使用%@
而不是%f
.但是请注意,您的消息字符串中只能有一个%@
(没有其他格式说明符),因为您只将一个参数传递给os_log
.输出看起来像这样:
Take 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 to os_log
. The output looks like this:
2018-06-20 02:22:56.132704-0500 test[39313:6086331] WTF: (
"1.2345"
)
您还可以通过 https://bugreport.apple.com 提交增强请求雷达,要求提供os_logv
函数,但您不应指望它会很快实现.
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.
好的,你一直在读书.让我们来看看os_log
的含义.事实证明,Swift os_log
函数的实现是 Swift公开源代码:
Okay, you kept reading. Let's peek under the hood of os_log
. It turns out the implementation of the Swift os_log
function is part of the public Swift source code:
@_exported import os
@_exported import os.log
import _SwiftOSOverlayShims
@available(macOS 10.14, iOS 12.0, watchOS 5.0, tvOS 12.0, *)
public func os_log(
_ type: OSLogType,
dso: UnsafeRawPointer = #dsohandle,
log: OSLog = .default,
_ message: StaticString,
_ args: CVarArg...)
{
guard log.isEnabled(type: type) else { return }
let ra = _swift_os_log_return_address()
message.withUTF8Buffer { (buf: UnsafeBufferPointer<UInt8>) in
// Since dladdr is in libc, it is safe to unsafeBitCast
// the cstring argument type.
buf.baseAddress!.withMemoryRebound(
to: CChar.self, capacity: buf.count
) { str in
withVaList(args) { valist in
_swift_os_log(dso, ra, log, type, str, valist)
}
}
}
}
因此,事实证明是的os_log
版本,称为_swift_os_log
,它带有预先捆绑的参数. Swift包装器使用withVaList
(已记录)将Array<CVarArg>
转换为va_list
,并将其传递给_swift_os_log
,而这本身也是
So it turns out there is a version of os_log
, called _swift_os_log
, that takes pre-bundled arguments. The Swift wrapper uses withVaList
(which is documented) to convert the Array<CVarArg>
to a va_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.
无论如何,即使没有记录,我们实际上也可以调用_swift_os_log
.我们基本上可以复制os_log
的源代码并将其转换为您的logDefault
函数:
Anyway, even though it's not documented, we can actually call _swift_os_log
. We can basically copy the source code of os_log
and turn it into your logDefault
function:
func logDefaultHack(_ message: StaticString, dso: UnsafeRawPointer = #dsohandle, _ args: CVarArg...) {
let ra = _swift_os_log_return_address()
message.withUTF8Buffer { (buf: UnsafeBufferPointer<UInt8>) in
buf.baseAddress!.withMemoryRebound(to: CChar.self, capacity: buf.count) { str in
withVaList(args) { valist in
_swift_os_log(dso, ra, .default, .default, str, valist)
}
}
}
}
它有效.测试代码:
func testWrapper() {
logDefault("WTF: %f", 1.2345)
logDefault("WTF: %@", 1.2345)
logDefaultHack("Hack: %f", 1.2345)
}
输出:
2018-06-20 02:22:56.131875-0500 test[39313:6086331] WTF: 0.000000
2018-06-20 02:22:56.132704-0500 test[39313:6086331] WTF: (
"1.2345"
)
2018-06-20 02:22:56.132807-0500 test[39313:6086331] Hack: 1.234500
我会推荐这种解决方案吗?不.地狱不. os_log
的内部是实现细节,并且在以后的Swift版本中可能会发生变化.因此,不要像这样依靠他们.但是无论如何,在幕后找东西还是很有趣的.
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.
最后一件事.为何编译器不抱怨将Array<CVarArg>
转换为CVarArg
?为什么Kamran的建议(使用%@
)起作用?
One last thing. Why doesn't the compiler complain about converting Array<CVarArg>
to CVarArg
? And why does Kamran's suggestion (of using %@
) work?
事实证明,这些问题的答案是相同的:这是因为Array
可桥接"到Objective-C对象.具体来说:
It turns out these questions have the same answer: it's because Array
is "bridgeable" to an Objective-C object. Specifically:
-
基金会(在Apple平台)使
Array
符合_ObjectiveCBridgeable
协议.它通过返回NSArray
将Array
桥接到Objective-C.
Foundation (on Apple platforms) makes
Array
conform to the_ObjectiveCBridgeable
protocol. It implements bridging ofArray
to Objective-C by returning anNSArray
.
withVaList
函数要求每个CVarArg
到将其自身转换为其_cVarArgEncoding
.
The withVaList
function asks each CVarArg
to convert itself to its _cVarArgEncoding
.
默认实现对于同时符合_ObjectiveCBridgeable
和CVarArg
的类型的_cVarArgEncoding
,返回桥接的Objective-C对象.
The default implementation of _cVarArgEncoding
, for a type that conforms to both _ObjectiveCBridgeable
and CVarArg
, returns the bridging Objective-C object.
Array
与CVarArg
的一致性意味着编译器不会抱怨(默默地)将Array<CVarArg>
转换为CVarArg
并将其粘贴到另一个Array<CVarArg>
中.
The conformance of Array
to CVarArg
means the compiler won't complain about (silently) converting an Array<CVarArg>
to a CVarArg
and sticking it into another Array<CVarArg>
.
这种静默转换通常可能是一个错误(如您的情况),因此编译器对其进行警告是合理的,并允许您使用显式强制转换使警告静默(例如args as CVarArg
) .您可以根据需要在 https://bugs.swift.org 上提交错误报告.
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.
这篇关于为什么包装os_log()导致double不能正确记录?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!