Os_log crash when forwarding [CVarArg] vs variadic CVarArg

Hi all,

I’m running into a crash when trying to wrap os_log in a helper function that takes an @autoclosure message and arguments. Here’s a simplified version:

import Foundation
import os.log

let testLog: OSLog = OSLog(subsystem: "test", category: "Test")

func runtimeIssues(
    _ message: @autoclosure () -> StaticString,
    _ args: @autoclosure () -> [CVarArg] = []
) {
    // ❌ This will crash:
    // os_log(.fault, log: testLog, message(), args())

    // ✅ But if I unsafeBitCast os_log to a version that accepts [CVarArg],
    // it works without crashing:
    unsafeBitCast(
        os_log as (OSLogType, UnsafeRawPointer, OSLog, StaticString, CVarArg...) -> Void,
        to: ((OSLogType, UnsafeRawPointer, OSLog, StaticString, [CVarArg]) -> Void).self
    )(.fault, #dsohandle, testLog, message(), args())
}

runtimeIssues("Error %s", ["test"])

When I call the function directly with os_log(.fault, log: testLog, message(), args()), I get a runtime crash:

Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '-[Swift.__SwiftDeferredNSArray UTF8String]:
unrecognized selector sent to instance 0x600000de2780'

But if I bitcast os_log to a version that takes [CVarArg], everything works fine.


Context

I’d like args to be @autoclosure, which means I can’t use CVarArg... directly in the parameter list (since variadics can only appear at the call site, not as return values). That’s why I ended up with @autoclosure () -> [CVarArg].

So my questions are:

  1. What’s the fundamental difference between CVarArg... and [CVarArg] here that causes the crash?
  2. Is the unsafeBitCast workaround the “right” way to make this work, or is there a safer/official way?
  3. Is there a Swift-native approach to forwarding variadic CVarArg... without this crash?

Any insight into why this happens would be appreciated.

Oh, I see. It seems that it is passing my "[CVarArg]" as a single "CVarArg" to the function. :joy:

Then when os_log try to call UTF8String for each of the CVarArg element, the array will crash.

This isn’t an answer to your question, but rather a word of caution: If you use a wrapper function like this, you lose a key feature of unified logging, namely the ability to right-click on the log message in the Xcode console and “jump to source” to take you to where the original log message was issued; it always jumps you to the utility function rather than where the message was originally generated. I played around with something like this sort of helper function, and later abandoned it for this reason. (And once you integrate this “jump to source” functionality into your workflow, you generally never want to go back.)

Theoretically, one could use a macro rather than a function to address this problem, but I personally never got my attempt at that working to my satisfaction.

1 Like

As far as I'm aware, Swift functions taking Thing... and [Thing] have the exact same calling convention, just different mangling and different source spelling, so the cast "should" be safe for functions written in Swift at least. I'm not as sure about os_log. I agree that the ability to forward variadic args is sorely missing from the language, and even surfaces in other ways that don't involve @autoclosure or CVarArg.

The usual way to handle this in C is to expose two versions of the function, one that takes variadic args and one that takes a raw va_list pointer (e.g. printf vs vprintf). Unfortunately I don't see an equivalent of os_log that takes a CVaListPointer.

Is that last paragraph a copy error from some sort of AI chatbot?

3 Likes

Got it. Thanks for the context here.

Yeah. Sorry about that. I was posting it before going to bed in the early morning, forgot to audit it.