How to fetch system information with sysctl in Swift on macOS
Sometimes you need to get deeper information about the system your program is running on (e.g. the number of cores, amount of memory, or even information about the entropy in the system) to make optimizations or to check if the requirements for your software are fulfilled.
This article will show an easy way on how to read system information and make use of it inside a Swift program.
Important: The code from this article will only work on macOS and not on iOS, iPadOS, watchOS, or visionOS. Running this example on these platforms will result in empty data.
Fetching system information
To fetch system information we need to make use of an Objective-C function:
int sysctlbyname(const char *, void *, size_t *, void *, size_t);
The signature of the function may not be very intuitive for Swift developers so I’ll explain it a bit.
Function parameters
The first parameter for the function is a char pointer. This parameter is for the key (or name) we want to fetch the data for (e.g. hw.machine, etc.) but more on these keys/names in a bit. The nearest comparison in Swift would be a String.
The second parameter is a void pointer which points to the memory location where we want to store the data to fetch. We have to reserve this space ourselves, next. The type is void to allow any type of data. The nearest Swift comparison would be Any.
The third parameter is the size of the data we want to store. As the previous parameter is void, there is no predefined bit size for our type, so we need to specify the size of bytes we want to store ourselves. The function cannot do that for us. This parameter is basically just an Int.
We don’t use the last two parameters so I won’t get any deeper into them. They are for setting new values, which we don’t want use, here.
If you want to read more about the function itself, see the official documentation: https://developer.apple.com/documentation/kernel/1387446-sysctlbyname
What does this function do?
The function we are calling is basically a wrapper for the unix program sysctl, which is a simple tool to get and set kernel state variables.
If you run sysctl on your command line without any arguments it will not be very useful as it only tells you to pass some:
➜ ~ sysctl
usage: sysctl [-bdehiNnoqx] name[=value] ...
sysctl [-bdehNnoqx] -a
To make use of this program you’ll need to pass keys/names to it or you can pass the parameter -a to get a list for all the available keys and their values on your system instead:
➜ ~ sysctl -a
user.cs_path: /usr/bin:/bin:/usr/sbin:/sbin
user.bc_base_max: 99
user.bc_dim_max: 2048
user.bc_scale_max: 99
user.bc_string_max: 1000
user.coll_weights_max: 2
user.expr_nest_max: 32
user.line_max: 2048
user.re_dup_max: 255
user.posix2_version: 200112
user.posix2_c_bind: 0
user.posix2_c_dev: 0
user.posix2_char_term: 0
user.posix2_fort_dev: 0
...
The complete list is quite long and contains a lot of information. You should have a look yourself by running sysctl -a on your machine.
From that list, you can pick the key(s) you need.
For our simple example we will focus on the key “hw.machine”, which defines the type of CPU the program is running on. This key will give us a string with a value of either x86_64 on Intel-based Macs or arm64 on Apple Silicon Macs.
Let’s write some code
As mentioned before we need to make use of an Objective-C function inside our Swift program. Fortunately, this is very simple as the function can be called directly without any bridges or helpers.
Our first task is to determine the size of the data for our key. As we saw before the function expects us to pass the amount of bytes we expect for the passed key.
To determine this size, we first create a variable to hold it and then call the function sysctlbyname from before to write the size to it. This can be done by passing nil as a third parameter (the pointer which would hold the data for the key):
var size = 0
sysctlbyname("hw.machine", nil, &size, nil, 0)
This call will store the size for the data of the key “hw.machine” in our variable size of type Int.
Notice the & (ampersand) prefix of our third paramter for the function. As this function expects a pointer we pass a reference to the value, instead of a copy. The same goes for the value which we’ll focus on next.
Note: It is important that the third parameter is nil. Otherwise, the function will not return the size but try to store the value for the key itself. You always have to fetch the size first — also if you fetch multiple keys in your program. Keep that in mind.
Next, we need to fetch the data for the key itself. In the case of “hw.machine” it’s a string, so we declare a variable for that and call sysctlbyname again:
var value = [CChar](repeating: 0, count: size)
sysctlbyname(key, &value, &size, nil, 0)
As you can see, we did not declare the variable value as a typical Swift String. This is because we don’t use Swift in that case as we’re making use of Objective-C which does not know about Swift Strings at all. Instead, we create an array of type CChar.
Now we can simply use and print the value in our Swift program — or can we?
Actually, we can’t. There is still one more step required to make the result we got really useful. We need to convert the CChar array we got to a Swift String object.
Converting the value
To get a String from a CChar is also very simple. The Swift String object provides an initializer for a CChar:
init(cString nullTerminatedUTF8: UnsafePointer<CChar>)
To convert our value we simply have to make use of that initializer and we can print it out:
print(String(cString: value))
The complete example
Let’s have a look at a complete example. To make it more convenient I created a simple struct to fetch the system information and return it.
I also print out another key “kern.osproductversion”, which is the version of the macOS the program is running on:
import Foundation
struct MachineInfo {
static func systemInfo(for key: String) -> String {
var size = 0
sysctlbyname(key, nil, &size, nil, 0)
var value = [CChar](repeating: 0, count: size)
sysctlbyname(key, &value, &size, nil, 0)
return String(cString: value)
}
}
print("CPU type: \(MachineInfo.systemInfo(for: "hw.machine"))")
print("OS version: \(MachineInfo.systemInfo(for: "kern.osproductversion"))")
Running this code will result in an output like:
➜ swift machineTest.swift
CPU type: arm64
OS version: 13.5
Note: Depending on your OS version and CPU type, the results can differ.