🍁

Swift: macのCPU温度を取得する(Intel編)

2021/05/15に公開
1

RunCatのユーザーから度々CPUの温度を表示してよとリクエストをもらっていたので頑張ってみました。

import IOKit

extension FourCharCode {
    init(fromString value: StringLiteralType) {
        precondition(value.utf8.count == 4)
        self = value.utf8.reduce(0) { $0 << 8 + FourCharCode($1) }
    }
}

typealias DummyBytes = (UInt8, UInt8, UInt8, UInt8, UInt16,
                        UInt16, UInt16, UInt32, UInt32, UInt32)

typealias DataBytes = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
                       UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
                       UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
                       UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)

struct KeyInfo {
    var dataSize: IOByteCount = 0
    var dataType: UInt32 = 0
    var dataAttributes: UInt8 = 0
}

struct InOutStruct {
    var key: FourCharCode = 0
    var dummy: DummyBytes = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
    var keyInfo = KeyInfo()
    var padding: UInt16 = 0
    var result: UInt8 = 0
    var status: UInt8 = 0
    var data8: UInt8 = 0
    var data32: UInt32 = 0
    var bytes: DataBytes = (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)
}

func getCPUTemperature() -> Double {
    var conn:  io_connect_t = 0
    var result: kern_return_t = 0

    defer {
        // Close Connection
        if conn != 0 {
            IOServiceClose(conn)
        }
    }

    // Open Connection
    guard let matching = IOServiceMatching("AppleSMC") else { return 0 }
    let service = IOServiceGetMatchingService(kIOMasterPortDefault, matching)
    if service == MACH_PORT_NULL { return 0 }
    result = IOServiceOpen(service, mach_task_self_, 0, &conn)
    IOObjectRelease(service)
    guard result == kIOReturnSuccess else { return 0 }

    // Read CPU Temperature
    var inputStruct = InOutStruct()
    inputStruct.key = FourCharCode(fromString: "TC0P")
    inputStruct.keyInfo.dataSize = 2
    inputStruct.data8 = 5

    var outputStruct = InOutStruct()
    let inputSize = MemoryLayout<InOutStruct>.stride
    var outputSize = MemoryLayout<InOutStruct>.stride

    result = IOConnectCallStructMethod(conn, UInt32(2), &inputStruct, inputSize, &outputStruct, &outputSize)
    guard result == kIOReturnSuccess, outputStruct.result == 0 else { return 0 }

    // Formatting the Value of Temperature
    return Double(256 * Int(outputStruct.bytes.0) + Int(outputStruct.bytes.1)) / 256.0
}
実行例
let value = getCPUTemperature()
print("Temperature: \(value)°C") // Temperature: 50.5625°C

アプリで動かすにはEntitlementsに

<key>com.apple.security.temporary-exception.sbpl</key>
<array>
    <string>(allow iokit-open)</string>
</array>

を追加する必要があるみたいです。ただこれだとApp Storeのレビューで却下されるらしいのでダメですね。

所感

FourCharCodeとかIOByteCountとか見たことない型(typealias)が何個か出てきてワクワクしました。また、最終的な実装には用いていませんが、StaticStringという構造体があることを初めて知りました。IOKitは奥が深くて、しかもAppleの公式リファレンスがポンコツなので全容が掴めないものですが、先行例を見ながら試行錯誤していくのは発掘の楽しさがありますね。本実装のキモはIOConnectCallStructMethodですが、これのinputStructoutputStructの構造体の定義を最初に見つけた人のエスパー力には脱帽するしかありません。

てか、たかがCPUの温度を取得するだけで難易度高すぎませんか?

参考

Discussion