os_log wrapper crashed when using "%s"

I am trying to wrap os_log for logging in my iOS app like so:

import Foundation
import OSLog

enum LogCategory: String, CaseIterable {
    case viewCycle
    case tracking
    case api
}

struct Log {
    
    private static let logs = {
        return LogCategory.allCases
            .reduce(into: [LogCategory: OSLog]()) { dict, category in
                dict[category] = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "BIMB", category: category.rawValue)
            }
    }()

    static func debug(category: LogCategory, message: StaticString, _ args: CVarArg...) {
        logImpl(category: category, message: message, type: .debug, args)
    }

    static func info(category: LogCategory, message: StaticString, _ args: CVarArg...) {
        logImpl(category: category, message: message, type: .info, args)
    }
    
    static func notice(category: LogCategory, message: StaticString, _ args: CVarArg...) {
        logImpl(category: category, message: message, type: .default, args)
    }
    
    static func warning(category: LogCategory, message: StaticString, _ args: CVarArg...) {
        logImpl(category: category, message: message, type: .default, args)
    }
    
    static func error(category: LogCategory, message: StaticString, _ args: CVarArg...) {
        logImpl(category: category, message: message, type: .error, args)
    }
    
    static func critical(category: LogCategory, message: StaticString, _ args: CVarArg...) {
        logImpl(category: category, message: message, type: .fault, args)
    }
    
    private static func logImpl(category: LogCategory, message: StaticString, type: OSLogType, _ args: CVarArg...) {
        guard let log = logs[category] else {
            return
        }
        os_log(message, log: log, type: type, args)
    }
}

The problem is if I did this:

Log.debug(category: .tracking, 
          message: "Device ID: %s.", 
          UIDevice.current.identifierForVendor?.uuidString ?? "unknown")

it always crashed with this error:

2023-12-13 12:33:35.173798+0700 bimb-authenticate-ios[62740:928633] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Swift.__SwiftDeferredNSArray UTF8String]: unrecognized selector sent to instance 0x600000dcbbc0'

But if I just do it with os_log like this:

os_log("Device ID: %s.",
       log: OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "tracking"),
       type: .debug,
       UIDevice.current.identifierForVendor?.uuidString ?? "unknown")

it worked fine. Also if I changed %@ in my wrapper instead, it didn't crash, but the idfv is shown inside a pair of brackets like this:

Device ID: (
    "C0F906C8-CD73-44F6-86A1-A587248680D3" 
).`

But with os_log it is shown normally like this: Device ID: C0F906C8-CD73-44F6-86A1-A587248680D3.

Can you tell me what's wrong here? And how do I fix this?

Thanks.

NOTE: This is using os_log since the minimum version is iOS 11. I don't know why people advising me with using Logger instead.

Answered by DTS Engineer in 774708022

Your code is failing due to limitations in Swift’s ability to work with C varags functions. There’s simply no way to do the equivalent of what C does with the macros in <stdarg.h>.

I don’t think your top-level goal is achievable. Both of our system log APIs, the os_log(…) functions and the Logger type, relying on special case handling within the compiler.

IMO the best path forward is to raise your deployment target and switch to Logger. If you can’t do that, you should stick with calling os_log directly. If you want to improve its ergonomics, I suggest you explore Swift macros.

Using Swift macros for this also presents significant challenges but it has the advantage that, if you can get the macro to expand properly, it’ll behave correctly at runtime.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

Your code is failing due to limitations in Swift’s ability to work with C varags functions. There’s simply no way to do the equivalent of what C does with the macros in <stdarg.h>.

I don’t think your top-level goal is achievable. Both of our system log APIs, the os_log(…) functions and the Logger type, relying on special case handling within the compiler.

IMO the best path forward is to raise your deployment target and switch to Logger. If you can’t do that, you should stick with calling os_log directly. If you want to improve its ergonomics, I suggest you explore Swift macros.

Using Swift macros for this also presents significant challenges but it has the advantage that, if you can get the macro to expand properly, it’ll behave correctly at runtime.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

os_log wrapper crashed when using "%s"
 
 
Q