os_log
Swift
logging
debugging
programming

Why does wrapping os_log cause doubles to not be logged correctly?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

os_log is not just an ordinary variadic logging function. The Swift overlay relies on compiler support and strict format-string handling, so when you wrap it in your own generic variadic function, floating-point values such as Double can be encoded incorrectly or lose the special handling that direct os_log calls receive.

Why Direct os_log Calls Behave Differently

When you call os_log directly, Swift can inspect the format string and the arguments at compile time. That gives the unified logging system extra type information, privacy annotations, and optimized encoding behavior.

A wrapper often looks harmless:

swift
1import os.log
2
3func badLog(_ message: StaticString, _ args: CVarArg...) {
4    withVaList(args) { pointer in
5        os_log(message, arguments: pointer)
6    }
7}
8
9let value = 1.2345
10badLog("value = %f", value)

But this wrapper changes the call shape. The compiler no longer sees the original os_log invocation in the same direct way, so the special formatting path becomes less reliable.

Why Double Is Often Where It Breaks

Floating-point arguments are more sensitive than simple integers because C variadic calls involve ABI rules and type promotion details. A wrapper that forwards CVarArg values can compile while still producing wrong output for certain types.

That is why you may see integers log correctly while Double values come out as zero, garbage, or some other unexpected value.

The deeper point is that os_log is designed to be called directly with a static format string. It is not a perfect fit for arbitrary variadic forwarding.

Safer Approaches

The safest fix is not to wrap the old os_log formatting API generically. Either:

  • call os_log directly at the call site
  • wrap a preformatted string instead of forwarding arbitrary format arguments
  • use the newer Logger API on supported Apple platforms

A direct call keeps the compiler-aware path intact:

swift
1import os.log
2
3let log = OSLog(subsystem: "com.example.app", category: "network")
4let value = 1.2345
5
6os_log("value = %{public}.3f", log: log, type: .debug, value)

On newer systems, Logger is usually the better API:

swift
1import OSLog
2
3let logger = Logger(subsystem: "com.example.app", category: "network")
4let value = 1.2345
5
6logger.debug("value = \(value, privacy: .public)")

That avoids the fragile varargs forwarding pattern entirely.

If You Need a Wrapper Anyway

If you must centralize logging behavior, wrap higher-level messages rather than the raw varargs API. For example:

swift
1import OSLog
2
3struct AppLog {
4    static let logger = Logger(subsystem: "com.example.app", category: "general")
5
6    static func debug(_ message: String) {
7        logger.debug("\(message, privacy: .public)")
8    }
9}
10
11let value = 1.2345
12AppLog.debug("value = \(value)")

This loses some of the structured format-string benefits of direct static logging, but it is far safer than trying to recreate os_log internals in a helper.

Modern Guidance

On current Apple platforms, Logger is the path worth favoring for new Swift code. It integrates better with interpolation, privacy control, and modern Swift syntax than the older C-style os_log formatting calls.

If you remain on the older API for deployment-target reasons, keep wrappers minimal and avoid generic forwarding of %f, %d, and mixed-format varargs unless you have tested every case carefully.

Common Pitfalls

The biggest mistake is assuming os_log behaves like printf plus a convenience wrapper. It has extra compiler-aware behavior that a normal helper function does not preserve.

Another issue is using a dynamic format string. os_log expects a static format string for safety and performance reasons.

Developers also often try to forward [CVarArg] or va_list as if the logging system were type-agnostic. That can break structured logging semantics even when the code compiles.

Finally, if you only need centralized logging and not the old format-string API, do not fight os_log. Use Logger or a wrapper around plain strings instead.

Summary

  • Direct os_log calls get compiler-aware handling that wrappers can lose.
  • 'Double values often expose the problem because varargs forwarding is type-sensitive.'
  • Generic wrappers around old os_log formatting are fragile.
  • Prefer direct os_log calls or the newer Logger API.
  • If you wrap logging, wrap final messages rather than raw C-style varargs.

Course illustration
Course illustration

All Rights Reserved.