Accessing macOS System Information

From Free Pascal wiki

Sysctl provides an interface that allows you to read and, with appropriate privileges, set many kernel attributes in macOS (and Linux and the BSDs). This provides a wealth of information detailing system hardware and configuration attributes which can be useful when debugging or simply optimising your application (eg to take advantage of threads on multi-core CPU systems).

The '/usr/local/share/fpcsrc/rtl/darwin/sysctlh.inc' source file is a partial translation of the macOS '/usr/include/sys/sysctl.h' file. For details of the sysctls available, start a Terminal and type 'man sysctl'. Typing 'sysctl-a | more' in a Terminal will list all the sysctls.

FPsysctl()

The fpSysCtl() function allows you to retrieve system information. The data returned from the function comprises integers (integer and int64), strings (AnsiStrings) and C structures (records). The function is defined in '/usr/local/share/fpcsrc/rtl/bsd/sysctl.pp' as:

function FPsysctl (Name: pchar; namelen:cuint; oldp:pointer;oldlenp:psize_t; newp:pointer;newlen:size_t):cint; cdecl; external name 'sysctl';

name -- a pointer to a Management Information Base (MIB) array of integers. Each element of this array must contain the correct value to read or write the particular system attribute.

namelen -- the length of the array of integers in name.

Note: if the old attribute value is not required, the next two variables can be set to Nil and 0.

oldp -- a pointer to the buffer into which the value is copied.

oldlenp -- the size of the oldp buffer.

Note: if the new attribute value is not being set, the next two variables should be set to Nil and 0.

newp -- a pointer to the buffer containing the new value to be set.

newlen -- the size of the newp buffer.


The size of the MIB array depends on the data to be read or written. The first element indicates the level of the information and the subsequent elements indicate the value to read. The top level names are:

          --------------------------------------------------------------
          Name              Next level names          Description
                            are found in
          --------------------------------------------------------------
          CTL_DEBUG         sys/sysctl.h              Debugging
          CTL_VFS           sys/mount.h               File system
          CTL_HW            sys/sysctl.h              Generic CPU, I/O
          CTL_KERN          sys/sysctl.h              High kernel limits
          CTL_MACHDEP       sys/sysctl.h              Machine dependent
          CTL_NET           sys/socket.h              Networking
          CTL_USER          sys/sysctl.h              User-level
          CTL_VM            sys/resources.h           Virtual memory
          --------------------------------------------------------------

Let's look at CTL_HW. The subsequent elements are found in the system sysctl.h file and are:

          -------------------------------------------------------------------------------          
          Name                     Value          Description
          -------------------------------------------------------------------------------          
          #define HW_MACHINE       1              /* string: machine class */
          #define HW_MODEL         2              /* string: specific machine model */
          #define HW_NCPU          3              /* int: number of cpus */
          #define HW_BYTEORDER     4              /* int: machine byte order */
          #define HW_PHYSMEM       5              /* int: total memory */
          #define HW_USERMEM       6              /* int: non-kernel memory */
          #define HW_PAGESIZE      7              /* int: software page size */
          #define HW_DISKNAMES     8              /* strings: disk drive names */
          #define HW_DISKSTATS     9              /* struct: diskstats[] */
          #define HW_EPOCH        10              /* int: 0 for Legacy, else NewWorld */
          #define HW_FLOATINGPT   11              /* int: has HW floating point? */
          #define HW_MACHINE_ARCH 12              /* string: machine architecture */
          #define HW_VECTORUNIT   13              /* int: has HW vector unit? */
          #define HW_BUS_FREQ     14              /* int: Bus Frequency */
          #define HW_CPU_FREQ     15              /* int: CPU Frequency */
          #define HW_CACHELINE    16              /* int: Cache Line Size in Bytes */
          #define HW_L1ICACHESIZE 17              /* int: L1 I Cache Size in Bytes */
          #define HW_L1DCACHESIZE 18              /* int: L1 D Cache Size in Bytes */
          #define HW_L2SETTINGS   19              /* int: L2 Cache Settings */
          #define HW_L2CACHESIZE  20              /* int: L2 Cache Size in Bytes */
          #define HW_L3SETTINGS   21              /* int: L3 Cache Settings */
          #define HW_L3CACHESIZE  22              /* int: L3 Cache Size in Bytes */
          #define HW_TB_FREQ      23              /* int: Bus Frequency */
          #define HW_MEMSIZE      24              /* uint64_t: physical ram size */
          #define HW_AVAILCPU     25              /* int: number of available CPUs */
          #define HW_MAXID        26              /* number of valid hw ids */
          -------------------------------------------------------------------------------

Armed with this information, we can now find out the number of CPUs:

...
Uses
  sysctl;
...
function NumberOfCPU: Integer;

var
  mib: array[0..1] of Integer;
  status : Integer;
  len : size_t;

begin
  mib[0] := CTL_HW;
  mib[1] := HW_NCPU;

  len := SizeOf(Result);
  status := fpSysCtl(PChar(@mib), Length(mib), @Result, @len, Nil, 0);
  if status <> 0 then RaiseLastOSError;
end;

The above code returns the number of CPUs in an integer; you need to check the C header file to determine what is being returned (an integer, int64, string, struct/record).

There is a possible issue with the above because if you have, for example, an Intel CPU with hyperthreading. One guess. Yes, it will return the total number of threads and not the number of CPUs or even the number of physical cores. HW_NCPU is pretty much deprecated these days as a useful attribute. Instead, we can retrieve the number of CPU packages, physical CPUs (cores) or logical CPUs (the same as HW_NCPU). To do so requires the use of a second function 'FPsysctlbyname'.

However, before we move on, let's look at retrieving a string with FPsysctl() which is a little different than retrieving an integer.

...
Uses
  sysctl;
...
function HWmodel : AnsiString;

var
  mib : array[0..1] of Integer;
  status : Integer;
  len : size_t;
  p   : PChar;

begin
 mib[0] := CTL_HW;
 mib[1] := HW_MODEL;

 status := fpSysCtl(PChar(@mib), Length(mib), Nil, @len, Nil, 0);
 if status <> 0 then RaiseLastOSError;

 GetMem(p, len);

 try
   status := fpSysCtl(PChar(@mib), Length(mib), p, @len, Nil, 0);
   if status <> 0 then RaiseLastOSError;
   Result := p;
 finally
   FreeMem(p);
 end;
end;

Notice that this time we needed two calls to FPsysctl() - one to find out the size of the buffer required to hold the string value, and the second to actually retrieve the string value into that buffer. On my Apple computer this returns the string "Macmini8,1" - the 2018 Mac mini.

No, we're not quite ready to move on yet... we still need to see how to retrieve information in a C structure (Pascal record).

...
Uses
  sysctl;
...
function SwapUsage: String;

type
 swapinfo = record
    xsu_total    : Int64;
    xsu_avail    : Int64;
    xsu_used     : Int64;
    xsu_pagesize : Integer;
    xsu_encrypted: Boolean;
end;

const
  VM_SWAPUSAGE = 5;

var
  mib : array[0..1] of Integer;
  status : Integer;
  len : size_t;
  swap : swapinfo;
  SwapEncrypted: String;

begin
  mib[0] := CTL_VM;
  mib[1] := VM_SWAPUSAGE;

  FillChar(swap, sizeof(swap), 0);
  len := sizeof(swap);

  status := fpSysCtl(PChar(@mib), Length(mib), @swap, @len, Nil, 0);
  if status <> 0 then RaiseLastOSError;

  if(swap.xsu_encrypted = true) then
    SwapEncrypted := 'Yes' else SwapEncrypted := 'No';

  Result := 'Swap total: ' + FloatToStr(Round(swap.xsu_total /1024 /1024)) + ' MB'
            + LineEnding + 'Swap used: ' + FloatToStr(Round(swap.xsu_used /1024 /1024)) + ' MB'
            + LineEnding + 'Swap free: ' + FloatToStr(Round(swap.xsu_avail /1024 /1024)) + ' MB'
            + LineEnding + 'Swap page size: ' + IntToStr(swap.xsu_pagesize) + ' bytes'
            + LineEnding + 'Swap encrypted: ' + SwapEncrypted + LineEnding;
end;

On my computer this returned:

 Swap total: 1024 MB
 Swap used: 191 MB
 Swap free: 833 MB
 Swap page size: 4096 bytes
 Swap encrypted: Yes

The swapinfo record was pretty easily translated from the C structure found in the system sysctl.h file:

struct xsw_usage {
	u_int64_t       xsu_total;
	u_int64_t       xsu_avail;
	u_int64_t       xsu_used;
	u_int32_t       xsu_pagesize;
	boolean_t       xsu_encrypted;
};

We also defined the MIB constant VM_SWAPUSAGE by looking at the system sysctl.h file. Why? Because Free Pascal 3.04 was missing this identifier for some reason. So, we looked it up and found:

#define VM_SWAPUSAGE    5               /* total swap usage */

Finally, yes, it is time to move on to the FPsysctlbyname() function.


FPsysctlbyname

The FPsysctlbyname function is defined in '/usr/local/share/fpcsrc/rtl/bsd/sysctl.pp' as:

function FPsysctlbyname (Name: pchar; oldp:pointer;oldlenp:psize_t; newp:pointer;newlen:size_t):cint; cdecl; external name 'sysctlbyname';

There. Not realy different from the FPsysctl() function. Is that a sigh of relief? So let's look at finally retrieving the number of physical CPU cores.

function NumberOfCores: Integer;

var
  status : Integer;
  len : size_t;

begin
  len := SizeOf(Result);

  status := fpSysCtlByName('hw.physicalcpu', @Result, @len, Nil, 0);
  if status <> 0 then RaiseLastOSError;
end;

On my 2018 Mac mini this returns the correct number of cores: 6. Where do you find the special incantation 'hw.physicalcpu' ? Yes, in the system sysctl.h file. Here's a brief excerpt of some of the hardware selectors:

 These are the support HW selectors for sysctlbyname.  Parameters that are byte counts or frequencies are 64 bit numbers.
 All other parameters are 32 bit numbers.
 
 hw.memsize                - The number of bytes of physical memory in the system.
 hw.ncpu                   - The maximum number of processors that could be available this boot.
                             Use this value for sizing of static per processor arrays; i.e. processor load statistics.
 hw.activecpu              - The number of processors currently available for executing threads.
                             Use this number to determine the number threads to create in SMP aware applications.
                             This number can change when power management modes are changed.
 hw.physicalcpu            - The number of physical processors available in the current power management mode.
 hw.physicalcpu_max        - The maximum number of physical processors that could be available this boot
 hw.logicalcpu             - The number of logical processors available in the current power management mode.
 hw.logicalcpu_max         - The maximum number of logical processors that could be available this boot
 hw.tbfrequency            - This gives the time base frequency used by the OS and is the basis of all timing services.
                             In general is is better to use mach's or higher level timing services, but this value
                             is needed to convert the PPC Time Base registers to real time.
 hw.cpufrequency           - These values provide the current, min and max cpu frequency.  The min and max are for
 hw.cpufrequency_max       - all power management modes.  The current frequency is the max frequency in the current mode.
 hw.cpufrequency_min       - All frequencies are in Hz.
 hw.busfrequency           - These values provide the current, min and max bus frequency.  The min and max are for
 hw.busfrequency_max       - all power management modes.  The current frequency is the max frequency in the current mode.
 hw.busfrequency_min       - All frequencies are in Hz.
 hw.cputype                - These values provide the mach-o cpu type and subtype.  A complete list is in <mach/machine.h>
 hw.cpusubtype             - These values should be used to determine what processor family the running cpu is from so that
                             the best binary can be chosen, or the best dynamic code generated.  They should not be used
                             to determine if a given processor feature is available.
 hw.cputhreadtype          - This value will be present if the processor supports threads.  Like hw.cpusubtype this selector
                             should not be used to infer features, and only used to name the processors thread architecture.
                             The values are defined in <mach/machine.h>
 hw.byteorder              - Gives the byte order of the processor.  4321 for big endian, 1234 for little.
 hw.pagesize               - Gives the size in bytes of the pages used by the processor and VM system.
 hw.cachelinesize          - Gives the size in bytes of the processor's cache lines.
                             This value should be use to control the strides of loops that use cache control instructions
                             like dcbz, dcbt or dcbst.
 hw.l1dcachesize           - These values provide the size in bytes of the L1, L2 and L3 caches.  If a cache is not present
 hw.l1icachesize           - then the selector will return and error.
 hw.l2cachesize            -
 hw.l3cachesize            -
 hw.packages               - Gives the number of processor packages.

If you prefer, you can instead use the sysctl command line utility in a Terminal to list all the hw selectors with "sysctl hw.".

Retrieving a string value, rather than an integer, is again reminiscent of FPsysctl(). Two calls to FPsysctlbyname() are needed: (1) to find out the size of the buffer to hold the string to be returned and (2) to return the string value into that buffer. Don't forget to allocate memory for the buffer before the second call!

function BrandOfCPU: AnsiString;
var
  status : Integer;
  len : size_t;
  p : PChar;
begin
  status := fpSysCtlByName('machdep.cpu.brand_string', Nil, @len, Nil, 0);
  if status <> 0 then RaiseLastOSError;

  GetMem(p, len);

  try
    status := fpSysCtlByName('machdep.cpu.brand_string', p, @len, Nil, 0);
    if status <> 0 then RaiseLastOSError;
    Result := p;
  finally
    FreeMem(p);
  end;
end;

On my computer, this returns the very informative string: Intel(R) Core(TM) i7-8700B CPU @ 3.20GHz. Oh, you noticed, I changed the top level identifier from hw to machdep (machine dependent). Again, consult sysctl.h or use the sysctl command line utility in a Terminal.

Last words

For the optimised, performance fiends out there, the man page for these functions notes that the FPsysctl() function runs in about a third the time as the same request made via the FPsysctlbyname() function.

It should be noted that many of the sysctl attributes are read-only, but there are also quite a few that can be changed. I've left changing a sysctl attribute as an exercise for you dear reader. It should be simple if you study the definitions of the function parameters, paying particular attention to newp and newlen.