---
title: "Swift Tuples Drop Their Tail Padding"
url: https://deallocatedobjects.com/posts/swift-tuples-drop-their-tail-padding
author: Jake Marsh
published: 2026-11-02
updated: 2026-11-02
topics: ["Apple", "Xcode & Tools"]
description: "Multimeter can show your Mac’s fan speed. There’s no public API for that, so it asks the System Management Controller through IOKit: open the AppleSMC service, then call IOConnectCallStructMethod with a struct going in and the same kind of struct coming back."
---

# Swift Tuples Drop Their Tail Padding

By Jake Marsh, November 2, 2026. HTML: https://deallocatedobjects.com/posts/swift-tuples-drop-their-tail-padding

[Multimeter](https://multimeterapp.com) can show your Mac's fan speed. There's no public API for that, so it asks the System Management Controller through IOKit: open the `AppleSMC` service, then call `IOConnectCallStructMethod` with a struct going in and the same kind of struct coming back. The struct is `SMCKeyData_t`, and in C it's 80 bytes.

My first Swift version of that struct was 76 bytes. It compiled, nothing warned, and every call came back `0xe00002c2`, which is `kIOReturnBadArgument`. The cause is a rule about Swift layout that's easy to miss if most of your C interop goes through imported headers: a Swift tuple's size doesn't include its trailing padding, and the next field moves in to fill it.

### The C Struct

Here's the C declaration the SMC expects:

```c title="smc.h"
#include <stdint.h>
typedef struct { char major; char minor; char build; char reserved[1]; uint16_t release; } SMCKeyData_vers_t;
typedef struct { uint16_t version; uint16_t length; uint32_t cpuPLimit; uint32_t gpuPLimit; uint32_t memPLimit; } SMCKeyData_pLimitData_t;
typedef struct { uint32_t dataSize; uint32_t dataType; char dataAttributes; } SMCKeyData_keyInfo_t;
typedef char SMCBytes_t[32];
typedef struct {
  uint32_t key;
  SMCKeyData_vers_t vers;
  SMCKeyData_pLimitData_t pLimitData;
  SMCKeyData_keyInfo_t keyInfo;
  char result;
  char status;
  char data8;
  uint32_t data32;
  SMCBytes_t bytes;
} SMCKeyData_t;
```

Three of the fields are small structs of their own. A tuple is the obvious way to write those in Swift without declaring three extra types, so that's what I did (`Bytes` is a typealias for a tuple of 32 `UInt8`s):

```swift
var key: UInt32 = 0
var vers = (UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt16(0))
var pLimitData = (UInt16(0), UInt16(0), UInt32(0), UInt32(0), UInt32(0))
var keyInfo = (UInt32(0), UInt32(0), UInt8(0))
var result: UInt8 = 0
var status: UInt8 = 0
var data8: UInt8 = 0
var data32: UInt32 = 0
var bytes: Bytes = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
```

`vers` and `pLimitData` happen to come out right. `keyInfo` doesn't.

### Size, Stride And Alignment

`MemoryLayout` gives you three numbers for any type, and they answer different questions:

- `size` is how many bytes the value's fields occupy, from the start to the end of the last field.
- `alignment` is the boundary the value has to start on.
- `stride` is `size` rounded up to a multiple of `alignment`, which is the distance from one element to the next in an array.

For a `(UInt32, UInt32, UInt8)` tuple, that's:

```swift
typealias KeyInfo = (UInt32, UInt32, UInt8)
print(MemoryLayout<KeyInfo>.size)      // 9
print(MemoryLayout<KeyInfo>.stride)    // 12
print(MemoryLayout<KeyInfo>.alignment) // 4
```

C has one number for this, `sizeof`, and it always includes the tail padding. `sizeof(SMCKeyData_keyInfo_t)` is 12, and a field that comes after a `keyInfo` starts 12 bytes after it.

Swift lays out the fields of a struct differently. Each field goes at the next offset that suits its own alignment after the previous field's `size`. So `result`, a `UInt8` with an alignment of 1, goes at offset 37, right after the tuple's 9 bytes, in the spot C keeps as padding.

The same thing happens if you declare `keyInfo` as a nested Swift struct with three properties. Its `size` is 9 and the next field lands at 9. Only a struct imported from C keeps C's answer: if Swift sees `SMCKeyData_keyInfo_t` through a header, its `size` is 12.

### Where The Bytes Went

Here are the field offsets from `offsetof` in C next to `MemoryLayout.offset(of:)` for the tuple version:

| Field | C | Swift with a tuple |
|-------|---|--------------------|
| `keyInfo` | 28 | 28 |
| `result` | 40 | 37 |
| `data32` | 44 | 40 |
| `bytes` | 48 | 44 |
| Total | 80 | 76 |

Everything after `keyInfo` is in the wrong place. `result`, `status` and `data8` move up three bytes; `data32` realigns to 40, four bytes early; the 32-byte payload starts at 44 and ends at 76.

The size alone is enough to get the call rejected. `SMCReader` passes `MemoryLayout<KeyData>.stride` as the input size, and for this version that's 76. On my MacBook Pro running macOS 27, sending a correctly laid out struct with an input size of 76 returns `kIOReturnBadArgument`, and the same struct with 80 succeeds.

### The Fix

In Multimeter's `SMCReader`, `keyInfo` is now three separate properties with the padding written out:

```swift title="Sources/Metrics/SystemReaders.swift"
// Mirrors the C SMCKeyData_t (80 bytes). keyInfo is spelled out field by
// field: as a Swift tuple it loses its tail padding and shifts
// everything after it by 3 or 4 bytes, and the SMC rejects every call.
var key: UInt32 = 0
var vers = (UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt16(0))
var pLimitData = (UInt16(0), UInt16(0), UInt32(0), UInt32(0), UInt32(0))
var keyInfoDataSize: UInt32 = 0
var keyInfoDataType: UInt32 = 0
var keyInfoAttributes: UInt8 = 0
var keyInfoPadding = (UInt8(0), UInt8(0), UInt8(0))
var result: UInt8 = 0
var status: UInt8 = 0
var data8: UInt8 = 0
var data32: UInt32 = 0
var bytes: Bytes = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
```

`keyInfoPadding` is the part that matters. Flattening the tuple without it would leave the same 76 bytes, since `result` would still sit right after `keyInfoAttributes`. With it, `result` is at 40, `bytes` is at 48, and `MemoryLayout<KeyData>.size` and `.stride` are both 80. The two-step read (key info with `data8 = 9`, then the value with `data8 = 5`) works, and the fan count key `FNum` comes back as a one-byte `ui8 `.

`vers` and `pLimitData` can stay tuples because neither has tail padding: `vers` is 6 bytes with 2-byte alignment and `pLimitData` is 16 with 4-byte alignment. That's luck of the layout, so I'd still check them the same way.

### Checking Against C

You don't have to work out offsets by hand. Put the header in a tiny C file and ask clang:

```c title="size.c"
#include <stdio.h>
#include <stddef.h>
#include "smc.h"
int main(void) {
  printf("SMCKeyData_t          %zu\n", sizeof(SMCKeyData_t));
  printf("SMCKeyData_keyInfo_t  %zu\n", sizeof(SMCKeyData_keyInfo_t));
  printf("offsetof keyInfo      %zu\n", offsetof(SMCKeyData_t, keyInfo));
  printf("offsetof result       %zu\n", offsetof(SMCKeyData_t, result));
  printf("offsetof data32       %zu\n", offsetof(SMCKeyData_t, data32));
  printf("offsetof bytes        %zu\n", offsetof(SMCKeyData_t, bytes));
  return 0;
}
```

```bash
clang size.c -o size && ./size
```

That prints 80, 12, 28, 40, 44 and 48. On the Swift side, `MemoryLayout<KeyData>.offset(of: \.bytes)` gives you the same kind of number for any stored property, so you can line the two lists up field by field.

### Mirroring Any C Struct In Swift

The best option is not to mirror it at all. If you can put the C declaration in a header, Swift imports the struct with C's layout. In an app target that's the bridging header (`SWIFT_OBJC_BRIDGING_HEADER`); in a package or a plain `swiftc` build, a module map works:

```text title="SMCShim/module.modulemap"
module SMCShim {
    header "smc.h"
    export *
}
```

After `import SMCShim`, `MemoryLayout<SMCKeyData_t>.size` is 80 and you get named fields like `data.keyInfo.dataSize` for free. The trade is that `char` fields import as `Int8` and the 32-byte array imports as a 32-element tuple of `Int8`.

If you do write the struct in Swift, avoid tuples and nested Swift structs for any C member that has tail padding, spell those fields out, and add the padding bytes yourself. Then make the layout prove itself. A few preconditions at startup or in a unit test turn a silent `kIOReturnBadArgument` into a crash that names the problem:

```swift
precondition(MemoryLayout<KeyData>.stride == 80, "KeyData is \(MemoryLayout<KeyData>.stride) bytes, SMCKeyData_t is 80")
precondition(MemoryLayout<KeyData>.offset(of: \.result) == 40)
precondition(MemoryLayout<KeyData>.offset(of: \.bytes) == 48)
```

Compare `stride` with `sizeof`, since `stride` is the Swift number that includes tail padding. For `KeyData` the two Swift numbers are both 80, but they often differ: a Swift struct with a `UInt32` followed by a `UInt8` has a `size` of 5 and a `stride` of 8, and C's `sizeof` for the same two fields is 8. The offset checks catch the other case, where fields shift around but the total happens to stay the same.
