Почему обертка os_log() приводит к тому, что парные номера не регистрируются правильно?

Рассмотрим следующий пример:

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()

let logger = OSLogWrapper()
logger.testWrapper()

Я получаю следующий вывод в консоли Xcode:

2018-06-19 18:21:08.327979-0400 WrapperWTF[50240:548958] WTF: 0.000000

Я проверил все, что мог придумать, и я не могу сделать головы или хвосты того, что здесь не так. Просмотр документации не принесет ничего полезного.

Спасибо за помощь!

Ответы

Ответ 1

Компилятор реализует вариативные аргументы, отбрасывая каждый аргумент объявленному вариационному типу, упаковывая их в Array такого типа и передавая этот массив вариационной функции. В случае testWrapper, заявленная VARIADIC тип CVarArg, поэтому, когда testWrapper называет logDefault, это то, что происходит под одеялом: testWrapper бросает 1.2345 к CVarArg, создает Array<CVarArg>, и передает его logDefault в args.

Затем logDefault вызывает os_log, передавая ему Array<CVarArg> в качестве аргумента. Это ошибка в коде. Ошибка довольно тонкая. Проблема в том, что os_log не принимает аргумент Array<CVarArg>; os_log сам по себе является вариационным по сравнению с CVarArg. Таким образом, Swift выдает args (Array<CVarArg>) в 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)

Затем 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 (принимает va_list, C эквивалент Array<CVarArg>)
  • NSLog (variadic) и NSLogv (принимает va_list)
  • -[NSString initWithFormat:] (variadic) и -[NSString WithFormat:arguments:] (принимает va_list)

Поэтому вы можете искать os_logv. К сожалению, вы его не найдете. Нет документального компаньона для os_log который принимает предварительно os_log аргументы.

На данный момент у вас есть два варианта:

  • Откажитесь от обертывания os_log в своей собственной вариационной оболочке, потому что просто нет хорошего способа это сделать или

  • Возьмите совет Камрана (в своем комментарии к своему вопросу) и используйте %@ вместо %f. Но учтите, что в вашей строке сообщения может быть только один %@ (и других спецификаторов формата), потому что вы передаете только один аргумент os_log. Результат выглядит следующим образом:

    2018-06-20 02:22:56.132704-0500 test[39313:6086331] WTF: (
        "1.2345"
    )
    

Вы также можете подать радары запроса на повышение по адресу 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)
      }
    }
  }
}

Так получается, есть версия os_log, называется _swift_os_log, который принимает заранее аргументы в комплекте. withVaList Swift использует withVaList (который документирован) для преобразования Array<CVarArg> в 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)
            }
        }
    }
}

И это работает. Тестовый код:

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. Поэтому не полагайтесь на них так. Но в любом случае интересно смотреть под обложки.


Одна последняя вещь. Почему компилятор не жалуется на преобразование Array<CVarArg> в CVarArg? И почему предложение Kamran (использования %@) работает?

Оказывается, эти вопросы имеют один и тот же ответ: это потому, что Array "мостик" для объекта Objective-C. В частности:

Это бесшумное преобразование, вероятно, часто является ошибкой (как это было в вашем случае), поэтому было бы разумно, чтобы компилятор предупредил об этом и позволил вам отключить предупреждение с явным приложением (например, args as CVarArg). Вы можете отправить отчет об ошибке на https://bugs.swift.org, если хотите.

Ответ 2

Как упоминалось в моем комментарии к ответу Роба Мейоффа выше, для тех, кто испытывает такую же проблему с os_signpost(), вот класс-обертка, который я сделал вокруг него:

import Foundation
import os
import _SwiftOSOverlayShims

public final class Signpost {

    private final let log: OSLog

    public init(log: OSLog) {
        self.log = log
    }

    public final func begin(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil) {
        if #available(iOS 12.0, *) {
            signpost(.begin, dso: dso, name: name, idObject: idObject)
        }
    }

    public final func begin(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil, _ format: StaticString, _ arguments: CVarArg...) {
        if #available(iOS 12.0, *) {
            signpost(.begin, dso: dso, name: name, idObject: idObject, format, arguments)
        }
    }

    public final func event(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil) {
        if #available(iOS 12.0, *) {
            signpost(.event, dso: dso, name: name, idObject: idObject)
        }
    }

    public final func event(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil, _ format: StaticString, _ arguments: CVarArg...) {
        if #available(iOS 12.0, *) {
            signpost(.event, dso: dso, name: name, idObject: idObject, format, arguments)
        }
    }

    public final func end(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil) {
        if #available(iOS 12.0, *) {
            signpost(.end, dso: dso, name: name, idObject: idObject)
        }
    }

    public final func end(name: StaticString, dso: UnsafeRawPointer = #dsohandle, idObject: AnyObject? = nil, _ format: StaticString, _ arguments: CVarArg...) {
        if #available(iOS 12.0, *) {
            signpost(.end, dso: dso, name: name, idObject: idObject, format, arguments)
        }
    }

    @available(iOS 12.0, *)
    private final func signpost(_ type: OSSignpostType, dso: UnsafeRawPointer = #dsohandle, name: StaticString, idObject: AnyObject? = nil) {
        guard log.signpostsEnabled else { return }
        let signpostID = getSignpostId(forObject: idObject)
        os_signpost(type, dso: dso, log: log, name: name, signpostID: signpostID)
    }

    @available(iOS 12.0, *)
    private final func signpost(
        _ type: OSSignpostType,
        dso: UnsafeRawPointer,
        name: StaticString,
        idObject: AnyObject? = nil,
        _ format: StaticString,
        _ arguments: [CVarArg])
    {
        // This crazy mess is because [CVarArg] gets treated as a single CVarArg and repassing a CVarArg... actually passes a [CVarArg]
        // This was copied from the publicly available Swift source code at https://github.com/apple/swift/blob/master/stdlib/public/Darwin/os/os_signpost.swift#L40
        // THIS IS A HACK
        guard log.signpostsEnabled else { return }
        let signpostID = getSignpostId(forObject: idObject)
        guard signpostID != .invalid && signpostID != .null else { return }
        let ra = _swift_os_log_return_address()
        name.withUTF8Buffer { (nameBuf: UnsafeBufferPointer<UInt8>) in
            // Since dladdr is in libc, it is safe to unsafeBitCast
            // the cstring argument type.
            nameBuf.baseAddress!.withMemoryRebound(to: CChar.self, capacity: nameBuf.count) { nameStr in
                format.withUTF8Buffer { (formatBuf: UnsafeBufferPointer<UInt8>) in
                    // Since dladdr is in libc, it is safe to unsafeBitCast
                    // the cstring argument type.
                    formatBuf.baseAddress!.withMemoryRebound(to: CChar.self, capacity: formatBuf.count) { formatStr in
                        withVaList(arguments) { valist in
                            _swift_os_signpost_with_format(dso, ra, log, type, nameStr, signpostID.rawValue, formatStr, valist)
                        }
                    }
                }
            }
        }
    }

    @available(iOS 12.0, *)
    private final func getSignpostId(forObject idObject: AnyObject?) -> OSSignpostID {
        if let idObject = idObject {
            return OSSignpostID(log: log, object: idObject)
        }
        return .exclusive
    }
}