simconnect-in-go.md
2025-07-03 | 6 min read

SimConnect in Go: Because Sometimes You Need Your Flight Sim API with a Side of Gopher

Flight SimulationGoProgramming

It started, as most things do, at around 11 PM with a half-finished cup of coffee and an idea that seemed completely reasonable at the time. I wanted to build a small Go utility that could pull live telemetry from Microsoft Flight Simulator. Simple enough, right?

Wrong. The SimConnect API — the official way to talk to MSFS — is a C++ SDK that ships as a DLL. The documentation is, let’s say, charmingly vintage. And the only Go implementation I could find was either abandoned, wrapped CGo in ways that made me deeply uncomfortable, or both.

The CGo Problem

CGo is Go’s escape hatch to call C code. It works, but it comes with a cost: cross-compilation becomes a nightmare, build times slow down, and you’re suddenly managing memory in two runtimes simultaneously. For a side project at 11 PM, that felt like too much punishment.

The SimConnect DLL uses a Windows handle-based API. You open a connection, register the data definitions you care about, and then pump a message loop to receive updates. The entire protocol lives in a single DLL — SimConnect.dll — which ships with MSFS.

Pure Go to the Rescue

Go has excellent Windows syscall support via golang.org/x/sys/windows. You can load DLLs, find procedures, and call them — all without touching CGo. The trick is getting the calling conventions right, which mostly involves fighting with unsafe.Pointer until it stops panicking.

go get golang.org/x/sys/windows

The basic pattern for calling SimConnect from pure Go looks like this:

var simConnect = windows.NewLazyDLL("SimConnect.dll")
var procOpen = simConnect.NewProc("SimConnect_Open")

func Open(name string) (windows.Handle, error) {
    var handle windows.Handle
    appName, _ := windows.UTF16PtrFromString(name)
    r, _, _ := procOpen.Call(
        uintptr(unsafe.Pointer(&handle)),
        uintptr(unsafe.Pointer(appName)),
        0, 0, 0, 0,
    )
    if r != 0 {
        return 0, fmt.Errorf("SimConnect_Open failed: %d", r)
    }
    return handle, nil
}

Each SimConnect function is loaded lazily from the DLL. The first call to any procedure loads the DLL if it isn’t already loaded. If SimConnect.dll isn’t present (i.e., MSFS isn’t installed), you get a clean error — not a crash.

How SimConnect Actually Works

SimConnect is a subscription-based protocol. You tell it what data you want, it sends you updates. There are three main concepts:

  • Data Definitions — register which simulation variables (SimVars) you want. Altitude, airspeed, heading, flap position, whatever MSFS exposes.
  • Requests — ask SimConnect to start sending you that data. Can be one-shot or continuous.
  • Message Dispatch — a loop that processes incoming messages from MSFS. Your data arrives here.

The message loop is where things get interesting. SimConnect can notify you via a Win32 event, a window message, or you can poll manually. For a CLI tool without a window, polling is the cleanest approach:

for {
    var ppData unsafe.Pointer
    var cbData uint32
    r, _, _ := procGetNextDispatch.Call(
        uintptr(handle),
        uintptr(unsafe.Pointer(&ppData)),
        uintptr(unsafe.Pointer(&cbData)),
    )
    if r != 0 || cbData == 0 {
        time.Sleep(10 * time.Millisecond)
        continue
    }
    processMessage(ppData, cbData)
}

Making It Actually Ergonomic

Raw SimConnect is verbose. Registering a single variable requires multiple steps, involves magic IDs you have to track yourself, and produces raw byte buffers you need to cast manually. I wanted something that felt like Go — not like a C++ API wrapped in a trenchcoat.

So the SDK provides a higher-level API on top: define a struct, tag the fields with SimVar names, register it in one call, and get typed data back via channels. No manual buffer casting, no ID juggling.

type FlightData struct {
    Altitude float64 `simvar:"INDICATED ALTITUDE,feet"`
    Airspeed float64 `simvar:"AIRSPEED INDICATED,knots"`
    Heading  float64 `simvar:"PLANE HEADING DEGREES MAGNETIC,degrees"`
}

ch, err := client.Subscribe[FlightData](ctx)
for data := range ch {
    fmt.Printf("ALT: %.0f ft | SPD: %.0f kts | HDG: %.0f°\n",
        data.Altitude, data.Airspeed, data.Heading)
}

That’s it. Three lines of actual business logic. The SDK handles the SimVar registration, request lifecycle, message parsing, and type casting under the hood.

Current Status and Caveats

The SDK covers the most common SimConnect operations: reading SimVars, writing SimVars, triggering events, and monitoring system state. It runs on MSFS 2020 and 2024 and is available on GitHub under the mrlm-net organization.

One important caveat: SimConnect.dll is only available on Windows and only when MSFS is installed. The SDK will fail to initialize cleanly if the DLL isn’t present — no panics, just a sensible error. Cross-platform support isn’t something SimConnect was ever designed for. This is a Windows-only party and the bouncer checks for DLLs at the door.

Is it production-ready? For a side project, absolutely. For a serious addon, probably — with the understanding that the SimConnect protocol is documented poorly enough that there are still edge cases I haven’t hit. But for reading telemetry and sending commands, it works beautifully.

Building this taught me more about Windows syscalls and unsafe pointer arithmetic than I ever wanted to know. But the result is a pure Go package that talks to one of the most popular flight simulators on the market, without a single line of C.

Totally worth the coffee at 11 PM.

[0] made by wanted.solutions *bash
~/blog/simconnect-in-go
YAML: still indentation-sensitive 04:20 CET