为什么包装os_log()导致双打不能正确记录?

Mik*_*ers 25 logging ios swift

请考虑以下示例:

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我创建一个新的实例OSLogWrapper并调用testWrapper()

let logger = OSLogWrapper()
logger.testWrapper()
Run Code Online (Sandbox Code Playgroud)

我在Xcode控制台中获得以下输出:

2018-06-19 18:21:08.327979-0400 WrapperWTF[50240:548958] WTF: 0.000000
Run Code Online (Sandbox Code Playgroud)

我已经检查了所有我能想到的东西,而且我无法对这里出了什么问题做出正面或反面.浏览文档并没有产生任何帮助.

谢谢您的帮助!

rob*_*off 58

编译器通过将每个参数转换为声明的可变参数类型,将它们打包成Array该类型的一个,并将该数组传递给可变参数函数来实现可变参数.在这种情况下testWrapper,声明的可变参数类型是CVarArg,所以当testWrapper调用时logDefault,这是在封面下发生的事情:testWrapper转换1.2345为a CVarArg,创建一个Array<CVarArg>,并将其传递给logDefaultas args.

然后logDefault调用os_log,将其Array<CVarArg>作为参数传递给它.这是您的代码中的错误.这个bug非常微妙.问题是os_log没有Array<CVarArg>争论; os_log本身是可变的CVarArg.因此,斯威夫特施放args(一Array<CVarArg>)CVarArg,并将其CVarArg投入另一个 Array<CVarArg>.结构如下所示:

Array<CVarArg> created in `logDefault`
  |
  +--> CVarArg (element at index 0)
         |
         +--> Array<CVarArg> (created in `testWrapper`)
                |
                +--> CVarArg (element at index 0)
                       |
                       +--> 1.2345 (a Double)
Run Code Online (Sandbox Code Playgroud)

然后logDefault传递这个新Array<CVarArg>os_log.所以你要求os_log格式化它的第一个元素,即(某种)Array<CVarArg>,使用%f,这是无意义的,你碰巧得到0.000000了输出.(我说"有点",因为这里有一些细微之处,我稍后会解释.)

因此,logDefault将其传入Array<CVarArg>作为潜在的多个可变参数之一传递给os_log,但实际上您logDefault要做的是将该传入Array<CVarArg>作为整个可变参数集传递给os_log,而不重新包装它.在其他语言中,这有时被称为"参数splatting".

可悲的是,Swift还没有任何关于参数splatting的语法.它在Swift-Evolution中被不止一次讨论过(例如在这个帖子中),但还没有解决方案.

这个问题的通常解决方案是寻找一个伴随函数,它将已经捆绑的可变参数作为单个参数.通常,伴侣会v添加到函数名称中.例子:

  • printf(variadic)和vprintf(取a va_list,C相当于Array<CVarArg>)
  • NSLog(可变的)和NSLogv(需要va_list)
  • -[NSString initWithFormat:](可变的)和-[NSString WithFormat:arguments:](需要va_list)

所以你可能会去找一个os_logv.可悲的是,你找不到一个.没有记录的伴随os_log物需要预先捆绑的参数.

此时您有两种选择:

  • 放弃os_log在你自己的可变包装器中包装,因为没有好的方法可以做到,或者

  • 以Kamran的建议(在他对你的问题的评论中)并使用%@而不是%f.但请注意,您%@的消息字符串中只能有一个(而不是其他格式说明符),因为您只传递一个参数os_log.输出如下所示:

    2018-06-20 02:22:56.132704-0500 test[39313:6086331] WTF: (
        "1.2345"
    )
    
    Run Code Online (Sandbox Code Playgroud)

您也可以在https://bugreport.apple.com上提交增强请求雷达,询问os_logv功能,但您不应期望它会在短时间内实施.

就是这样了.做这两件事中的一件,也许提出雷达,继续你的生活.认真.别在这里读.这一行之后没什么好处的.


好的,你一直在读.让我们一起来看看os_log.事实证明,Swift os_log函数的实现是公共Swift源代码的一部分:

@_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)
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

因此,原来在那里一个版本os_log,称为_swift_os_log,这需要预先绑定的参数.Swift包装器使用withVaList(记录在案)将Array<CVarArg>a 转换为a va_list并将其传递给它_swift_os_log,它本身也是公共Swift源代码的一部分.我不会在这里引用它的代码,因为它很长,我们实际上并不需要看它.

无论如何,即使它没有记录,我们实际上可以打电话_swift_os_log.我们基本上可以复制源代码os_log并将其转换为您的logDefault函数:

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)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它有效.测试代码:

func testWrapper() {
    logDefault("WTF: %f", 1.2345)
    logDefault("WTF: %@", 1.2345)
    logDefaultHack("Hack: %f", 1.2345)
}
Run Code Online (Sandbox Code Playgroud)

输出:

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
Run Code Online (Sandbox Code Playgroud)

我会推荐这个解决方案吗?不,地狱没有.内部结构os_log是一个实现细节,可能会在未来版本的Swift中发生变化.所以不要像这样依赖他们.但无论如何,看看封面是有趣的.


最后一件事.为什么编译器没有抱怨转换Array<CVarArg>CVarArg?为什么Kamran的建议(使用%@)有效?

事实证明,这些问题具有相同的答案:因为它Array与Objective-C对象"可桥接".特别:

这种静默转换可能通常是一个错误(就像你的情况一样),所以编译器警告它是合理的,并允许你用显式转换(例如args as CVarArg)来使警告静音.如果需要,您可以在https://bugs.swift.org上提交错误报告.

  • 伙计......哇....这是我在StackOverflow上得到的最好的答案,我已经在这里待了一段时间.我有一种预感,我遇到了一个微妙的Swift处理可变参数,所以谢谢你. (6认同)
  • @zgosalvez查看os_log的最新Swift源代码,你会看到你现在需要`import _SwiftOSOverlayShims` (3认同)
  • 我提交了一个Swift错误请求警告:https://bugs.swift.org/browse/SR-8047 (2认同)
  • 罗伯,你在这里打开了地狱之门。干杯! (2认同)