Using F# to interop with C libraries for hardware monitoring

Hardware monitoring often begins as a small requirement: read a temperature, check a fan, or alert when a storage device is approaching a dangerous threshold. In a mixed Linux and Windows environment, however, the useful interfaces are frequently exposed by C libraries rather than by .NET APIs. F# can work with those libraries effectively, provided the native boundary is designed with care.

This is especially relevant for Australian infrastructure, where a system may run in a Sydney office, a Brisbane plant room, or a small Perth site connected over NBN. Remote visibility matters when local technical support is limited, ambient temperatures vary widely, and replacement hardware may take longer to source outside the major metropolitan markets.

Why native monitoring interfaces matter

Operating systems expose some hardware data through standard facilities, but detailed readings often come from platform-specific tools. On Linux, lm-sensors, libudev, SMART utilities, and vendor libraries provide access to thermal sensors, voltage rails, fan speeds, disks, and device events. On Windows, similar information may be available through native DLLs, performance counters, WMI providers, or manufacturer SDKs.

Calling a C library from F# avoids launching a separate process for every reading and gives the monitoring service a more direct integration point. A long-running daemon can initialise the library once, poll at a sensible interval, and publish structured results to a log, Prometheus endpoint, or internal dashboard.

The approach also keeps the monitoring layer separate from the system under observation. A service can report that a storage controller is overheating without becoming entangled with the application that uses the controller. That separation is valuable for environments such as gaming venues, where a monitoring component should remain independent from application behaviour; a related pokies feature article provides useful context for why operational software often needs clear boundaries.

Designing a safe F# and C boundary

F# uses Platform Invocation Services, commonly called P/Invoke, to declare functions exported by native libraries. A simple declaration might look like this:

open System
open System.Runtime.InteropServices

module Native =
    [<DllImport("libexample.so",
                CallingConvention = CallingConvention.Cdecl,
                EntryPoint = "example_temperature")>]
    extern int readTemperature(IntPtr context, byref<float32> value)

The declaration must match the C ABI exactly. That includes the library name, symbol name, calling convention, integer width, floating-point representation, pointer use, and whether a function writes through an output parameter. A mismatch may produce an immediate access violation, or worse, a value that appears plausible while silently corrupting memory.

Use explicit native representations rather than relying on F# records to map automatically. A C structure containing fixed-width fields can be represented with a [<Struct; StructLayout(LayoutKind.Sequential)>] type and fields such as int32, uint32, int64, or nativeint. Variable-length strings, unions, packed structures, and bit fields deserve special attention because their layout is easy to misunderstand.

Native handles should be wrapped in a small managed type or SafeHandle implementation. The wrapper can guarantee that the corresponding release function runs when the object is disposed. This is safer than passing raw IntPtr values throughout the application, where it becomes difficult to know whether a pointer is still valid or who owns it.

Reading sensors without losing reliability

Many C monitoring APIs follow a pattern of initialisation, enumeration, reading, and shutdown. In F#, represent that lifecycle explicitly. Open the library once during service startup, enumerate chips or devices, cache stable identifiers, and release everything during shutdown. Avoid repeatedly initialising and closing a native library inside a timer callback.

A reading function should return a domain type rather than exposing native error codes to every caller. For example, a result might distinguish a valid temperature from a missing sensor, a permission failure, a transient I/O error, or an unsupported device. This makes alerting decisions clearer and prevents a failed fan reading from being mistaken for zero RPM.

type SensorReading =
    { Name: string
      Value: float
      Unit: string
      CollectedAt: DateTimeOffset }

type ReadError =
    | Unavailable
    | PermissionDenied
    | NativeFailure of int

Temperature sensors also require interpretation. A CPU package temperature, a drive temperature, and a motherboard ambient reading are different signals with different thresholds. Brisbane summer conditions can raise room temperatures significantly, while equipment in an unconditioned shed may experience rapid changes. Store the raw value, the sensor identity, and the timestamp so that later analysis can distinguish a real thermal trend from a mislabelled input.

Handling callbacks, pointers, and platform differences

Some libraries return data synchronously; others use callbacks for device changes or asynchronous events. When passing an F# function to C, create a delegate with the required signature and retain a strong reference to it for the entire period in which native code may call it. If the delegate is collected early, the next native callback can crash the process.

Callback code should do very little. Copy the native event into a managed record and place it on a queue or mailbox processor. Do not perform lengthy logging, network requests, or blocking locks inside a native callback. This keeps the boundary predictable and prevents a slow downstream system from stalling the library’s own event thread.

A production service may need separate native builds for Windows and Linux. Use conditional configuration for library names and isolate platform declarations behind an F# module. A monitoring agent deployed across Sydney and Melbourne offices might use different device drivers from one running on a Linux gateway in regional Queensland. Detect capabilities at startup and report unsupported sensors clearly instead of assuming every machine exposes the same layout.

Making the monitor suitable for operations

Hardware monitoring becomes useful when it produces dependable signals rather than a large volume of raw numbers. Give each measurement a stable name, unit, source, and quality state. Record the native error code in diagnostic logs, but present operators with a concise message such as “NVMe temperature unavailable” or “fan tachometer stopped responding”.

Polling frequency should match the risk. A five-second interval may be reasonable for a rapidly changing thermal problem, while disk health information may only need collection every few minutes. Add timeouts around native operations where the library permits them, and isolate a problematic device so one stalled read cannot prevent other sensors from being collected.

Alert thresholds should account for persistence and hysteresis. Triggering an alarm at 80°C and clearing it at 79.9°C creates noise; requiring several consecutive readings and clearing only after the value falls below a lower threshold gives operators a more useful signal. Keep times in UTC internally, then display local Australian time zones such as AEST, AEDT, or AWST according to the site configuration.

For small and medium businesses, deployment simplicity is often more important than an elaborate platform. Research from SMB technology research can help frame the operational realities of smaller organisations, where a lightweight service, clear documentation, and predictable resource usage may matter more than an extensive enterprise feature set.

A practical interop checklist

Before connecting an F# service to a native hardware library, verify the ABI and define the ownership rules. The following checks catch many faults before they reach production:

  • Confirm the native library version and exported symbol names on every target operating system.
  • Match C integer types with fixed-width F# and .NET types.
  • Document which functions allocate, release, or retain memory.
  • Represent nullable pointers and optional values explicitly.
  • Keep callback delegates alive for their full native lifetime.
  • Test malformed, missing, and partially populated sensor data.

A second set of checks belongs to operations rather than marshalling. These details determine whether the service remains useful after installation:

  • Run the agent under the least-privileged account that can read the required devices.
  • Record library errors with the host name, sensor identity, and collection timestamp.
  • Use bounded queues so a failed metrics destination cannot exhaust memory.
  • Test behaviour after suspend, reboot, device removal, and driver updates.
  • Package the correct native dependencies with the service or installation process.
  • Validate alerts against realistic heat, load, and cooling conditions.

F# is well suited to this work because it combines concise interop declarations with strong domain modelling and clear error-handling patterns. The native boundary remains inherently unsafe, but a narrow wrapper can contain that risk and expose a clean interface to the rest of the application.

Build the smallest useful adapter first: initialise one library, read one sensor, map its errors, and shut it down cleanly. Then add discovery, structured metrics, alert persistence, and platform-specific support as operational requirements emerge. That progression produces a monitoring tool that is easier to test, easier to deploy across Australian sites, and far less likely to confuse a native fault with a genuine hardware problem.

Experience

Information Technology Consulting

Independent Practice

Provides IT consulting services focused on infrastructure planning, cloud migration strategy, and systems architecture. Engagements draw on years of hands-on sysadmin and development experience across Linux, Windows, and hybrid environments.

K9 Search & Rescue Volunteer

Ongoing

Active participant in K9 Search & Rescue operations, combining technical logistics skills with field support for canine search teams.

Karl Katzke's Blog

October 2006 – May 2014

Published a long-running personal technology blog covering cloud vs. in-house infrastructure, F# and Mono on OSX, hardware vendor critiques, RAID card performance analysis, and sysadmin storytelling. Notable posts include "When Sysadmins Ruled the Earth" (May 15, 2014) and "Getting Started with F# and Mono on OSX" (December 22, 2012).

Credentials

A small badge icon with a shield shape in muted blue tones on a light background

Systems Administration

Deep experience with Linux (RHEL, SLES, CentOS), high-availability clusters, and STONITH configurations.

A small badge icon with a gear shape in muted blue tones on a light background

Cloud Infrastructure

Practical knowledge of AWS EC2, reserved instances, and cost analysis for cloud vs. on-premises deployments.

A small badge icon with a code symbol in muted blue tones on a light background

Development

Proficient in F#, PHP (Symfony), and cross-platform tooling including Mono and MonoDevelop on OSX.

Studies

F# & Functional Programming

Self-directed, 2012

Explored strongly typed functional programming with F# on OSX using the Mono runtime. Published a detailed getting-started guide covering toolchain setup and cross-platform game development research.

High-Availability & Cluster Management

Professional Development, 2009

Configured and documented crm_mon email alerting for STONITH events on SLES11-HAE clusters, integrating with Nagios monitoring for production environments.

Hardware & Storage Performance

Ongoing

Conducted hands-on benchmarking of SATA/SAS RAID controllers including HighPoint RocketRaid 2740 and LSI/SuperMicro AOC-USASLP2-H8iR, comparing against software RAID configurations.

Skills

A small icon representing a server with clean geometric lines in slate blue

Linux Administration

RHEL, SLES, CentOS — package management, kernel tuning, HA clustering, and monitoring integration.

A small icon representing a cloud shape with clean geometric lines in slate blue

Cloud Architecture

AWS EC2, reserved-instance planning, cost modeling, and hybrid infrastructure strategy.

A small icon representing code brackets with clean geometric lines in slate blue

F# & .NET/Mono

Functional programming on OSX, MonoDevelop toolchain, and cross-platform game-dev exploration.

A small icon representing a database cylinder with clean geometric lines in slate blue

PHP & Symfony

Web application development with the Symfony framework and the broader PHP ecosystem.

A small icon representing a storage drive with clean geometric lines in slate blue

Storage & RAID

SATA/SAS controller evaluation, md RAID configuration, and performance benchmarking.

A small icon representing a shield with clean geometric lines in slate blue

High Availability

Pacemaker, STONITH, crm_mon alerting, and Nagios integration for production cluster monitoring.