diff --git a/x/examples/objc/ProcessInfo.app/Info.plist b/x/examples/objc/ProcessInfo.app/Info.plist
new file mode 100644
index 00000000..2b10ed6d
--- /dev/null
+++ b/x/examples/objc/ProcessInfo.app/Info.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleExecutable
+ main
+
+ CFBundleIdentifier
+ org.getoutline.test
+
+ CFBundleName
+ ProcessInfo
+
+ CFBundlePackageType
+ APPL
+
+ CFBundleShortVersionString
+ 1.0
+
+ CFBundleVersion
+ 1
+
+ LSRequiresIPhoneOS
+
+
+
diff --git a/x/examples/objc/README.md b/x/examples/objc/README.md
new file mode 100644
index 00000000..cc498d1e
--- /dev/null
+++ b/x/examples/objc/README.md
@@ -0,0 +1,85 @@
+
+
+
+This works as macOS:
+
+```console
+$ GOOS=ios GOARCH=arm64 CGO_ENABLED=1 go -C x run ./examples/objc
+Attempting to get iOS process info using Cgo...
+
+--- Successfully Retrieved Process Info ---
+Process Name: objc
+Process ID (PID): 72134
+User Name:
+Full User Name:
+Globally Unique ID:
+OS Version: Version 15.6.1 (Build 24G90)
+Hostname:
+Is Mac Catalyst App: false
+Is iOS App on Mac: false
+Physical Memory (B): 17178869184
+System Uptime (s): 198843.16
+Processor Count: 10
+Active Processor Count: 10
+```
+
+This seems to properly build for iOS:
+
+```console
+% CC="$(xcrun --sdk iphoneos --find cc) -isysroot \"$(xcrun --sdk iphoneos --show-sdk-path)\"" GOOS=ios GOARCH=arm64 CGO_ENABLED=1 go -C x buildĀ -v ./examples/objc
+
+github.com/Jigsaw-Code/outline-sdk/x/examples/objc
+# github.com/Jigsaw-Code/outline-sdk/x/examples/objc
+examples/objc/process_info.go:74:47: error: 'userName' is unavailable: not available on iOS
+ 74 | p_info->userName = safe_strdup([[info userName] UTF8String]);
+ | ^
+/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.4.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSProcessInfo.h:192:38: note: property 'userName' is declared unavailable here
+ 192 | @property (readonly, copy) NSString *userName API_AVAILABLE(macosx(10.12)) API_UNAVAILABLE(ios, watchos, tvos);
+ | ^
+/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.4.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSProcessInfo.h:192:38: note: 'userName' has been explicitly marked unavailable here
+examples/objc/process_info.go:75:51: error: 'fullUserName' is unavailable: not available on iOS
+ 75 | p_info->fullUserName = safe_strdup([[info fullUserName] UTF8String]);
+ | ^
+/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.4.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSProcessInfo.h:193:38: note: property 'fullUserName' is declared unavailable here
+ 193 | @property (readonly, copy) NSString *fullUserName API_AVAILABLE(macosx(10.12)) API_UNAVAILABLE(ios, watchos, tvos);
+ | ^
+/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.4.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSProcessInfo.h:193:38: note: 'fullUserName' has been explicitly marked unavailable here
+2 errors generated.
+```
+
+
+For the simulator:
+
+```console
+% CC="$(xcrun --sdk iphonesimulator --find cc) -isysroot \"$(xcrun --sdk iphonesimulator --show-sdk-path)\"" GOOS=ios GOARCH=arm64 CGO_ENABLED=1 go -C x build -v ./examples/objc
+```
+
+If you build it for the iOS Simulator, you can run on the iOS simulator. It correctly returns the iOS version on the simulator (18.4), though notice that "Is iOS App on Mac" is false, because it's not "on Mac".
+
+```
+% CC="$(xcrun --sdk iphonesimulator --find cc) -isysroot \"$(xcrun --sdk iphonesimulator --show-sdk-path)\"" GOOS=ios GOARCH=arm64 CGO_ENABLED=1 go -C x build -v -o examples/objc/ProcessInfo.app ./examples/objc/main.go
+
+% xcrun simctl boot 529EC4D4-FFC6-4249-A829-0D8181639E9D
+
+% xcrun simctl install booted ./x/examples/objc/ProcessInfo.app
+
+% xcrun simctl launch --console booted org.getoutline.test
+org.getoutline.test: 43075
+Attempting to get iOS process info using Cgo...
+
+--- Successfully Retrieved Process Info ---
+Process Name: main
+Process ID (PID): 43075
+User Name:
+Full User Name:
+Globally Unique ID: 5A277847-FAB1-491C-930F-AEBFB8BC145C-43075-00000903D8910887
+OS Version: Version 18.4 (Build 22E238)
+Hostname: fortuna-macbookpro2.roam.internal
+Is Mac Catalyst App: false
+Is iOS App on Mac: false
+Physical Memory (B): 17179869184
+System Uptime (s): 413005.42
+Processor Count: 10
+Active Processor Count: 10
+-------------------------------------------
+```
diff --git a/x/examples/objc/main.go b/x/examples/objc/main.go
new file mode 100644
index 00000000..f42900ee
--- /dev/null
+++ b/x/examples/objc/main.go
@@ -0,0 +1,250 @@
+// Copyright 2025 The Outline Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build darwin
+
+package main
+
+/*
+// These Cgo directives are essential for compiling on an Apple platform.
+#cgo CFLAGS: -x objective-c
+#cgo LDFLAGS: -framework Foundation
+
+#import
+#include // For malloc, free, strdup
+
+// A C struct to hold all the process information we want to retrieve.
+// This allows us to get all the data in a single Cgo call.
+typedef struct {
+ char* processName;
+ int processIdentifier;
+ char* globallyUniqueString;
+ char* operatingSystemVersionString;
+ char* hostName;
+ unsigned long long physicalMemory;
+ double systemUptime;
+ int processorCount;
+ int activeProcessorCount;
+ // New fields from your request
+ int isMacCatalystApp;
+ int isiOSAppOnMac;
+ int isIOS;
+ char* userName;
+ char* fullUserName;
+} ProcessInfo_t;
+
+// A helper function to safely duplicate a C string that might be NULL.
+// strdup(NULL) is undefined behavior, so this prevents crashes.
+static char* safe_strdup(const char* s) {
+ if (s == NULL) {
+ // Return a dynamically allocated empty string.
+ return strdup("");
+ }
+ return strdup(s);
+}
+
+
+// This C function (using Objective-C) populates and returns a struct
+// containing a wide range of process information.
+static ProcessInfo_t* get_all_process_info() {
+ // Autorelease pool is good practice for managing memory in Objective-C code
+ // called from other languages.
+ @autoreleasepool {
+ NSProcessInfo *info = [NSProcessInfo processInfo];
+
+ // Allocate memory for our C struct.
+ ProcessInfo_t *p_info = (ProcessInfo_t*)malloc(sizeof(ProcessInfo_t));
+ if (p_info == NULL) {
+ return NULL; // Failed to allocate memory
+ }
+
+ // Use safe_strdup to create heap-allocated C string copies that Go can safely manage and free.
+ p_info->processName = safe_strdup([[info processName] UTF8String]);
+ p_info->globallyUniqueString = safe_strdup([[info globallyUniqueString] UTF8String]);
+ p_info->operatingSystemVersionString = safe_strdup([[info operatingSystemVersionString] UTF8String]);
+ p_info->hostName = safe_strdup([[info hostName] UTF8String]);
+
+ #if TARGET_OS_OSX
+ // NSUserName and NSFullUserName are only available on macOS.
+ // We use the modern, non-deprecated functions here.
+ p_info->userName = safe_strdup([NSUserName() UTF8String]);
+ p_info->fullUserName = safe_strdup([NSFullUserName() UTF8String]);
+ #else
+ // On other platforms (like iOS), provide empty strings to avoid crashes.
+ p_info->userName = safe_strdup(NULL);
+ p_info->fullUserName = safe_strdup(NULL);
+ #endif
+
+ // Populate numeric fields directly.
+ p_info->processIdentifier = [info processIdentifier];
+ p_info->physicalMemory = [info physicalMemory];
+ p_info->systemUptime = [info systemUptime];
+ p_info->processorCount = (int)[info processorCount];
+ p_info->activeProcessorCount = (int)[info activeProcessorCount];
+
+ // Populate boolean flags (as integers), checking for API availability.
+ if (@available(macOS 10.15, iOS 13.0, *)) {
+ p_info->isMacCatalystApp = [info isMacCatalystApp] ? 1 : 0;
+ } else {
+ p_info->isMacCatalystApp = 0; // Default to false on older systems.
+ }
+
+ #if TARGET_OS_IOS
+ p_info->isIOS = 1;
+ #else
+ p_info->isIOS = 0;
+ #endif
+
+ if (@available(macOS 11.0, iOS 14.0, *)) {
+ p_info->isiOSAppOnMac = [info isiOSAppOnMac] ? 1 : 0;
+ } else {
+ p_info->isiOSAppOnMac = 0; // Default to false on older systems.
+ }
+
+ return p_info;
+ }
+}
+*/
+import "C"
+import (
+ "fmt"
+ "log"
+ "log/slog"
+ "os"
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+)
+
+// A Go struct that mirrors the C struct, providing an idiomatic way
+// to work with the process information in Go.
+type ProcessInfo struct {
+ ProcessName string
+ ProcessIdentifier int
+ GloballyUniqueString string
+ OperatingSystemVersionString string
+ HostName string
+ PhysicalMemoryBytes uint64
+ SystemUptimeSeconds float64
+ ProcessorCount int
+ ActiveProcessorCount int
+ IsMacCatalystApp bool
+ IsIOSAppOnMac bool
+ IsIOS bool
+ UserName string
+ FullUserName string
+}
+
+// getProcessInfo is a Go wrapper function that calls the underlying C function
+// and converts the C struct into a Go struct.
+func getProcessInfo() (*ProcessInfo, error) {
+ // Call the C function to get the populated struct.
+ cInfo := C.get_all_process_info()
+
+ // Check if the C function returned NULL, which indicates an error.
+ if cInfo == nil {
+ return nil, fmt.Errorf("failed to get process info from NSProcessInfo")
+ }
+
+ // The memory for the C struct and its string members was allocated in C.
+ // We must free all of it to prevent memory leaks. The defer statements
+ // ensure C.free is called for each allocated piece of memory right
+ // before the function returns.
+ defer C.free(unsafe.Pointer(cInfo.processName))
+ defer C.free(unsafe.Pointer(cInfo.globallyUniqueString))
+ defer C.free(unsafe.Pointer(cInfo.operatingSystemVersionString))
+ defer C.free(unsafe.Pointer(cInfo.hostName))
+ defer C.free(unsafe.Pointer(cInfo.userName))
+ defer C.free(unsafe.Pointer(cInfo.fullUserName))
+ defer C.free(unsafe.Pointer(cInfo))
+
+ // Create a Go struct and copy the data from the C struct, converting types as needed.
+ goInfo := &ProcessInfo{
+ ProcessName: C.GoString(cInfo.processName),
+ ProcessIdentifier: int(cInfo.processIdentifier),
+ GloballyUniqueString: C.GoString(cInfo.globallyUniqueString),
+ OperatingSystemVersionString: C.GoString(cInfo.operatingSystemVersionString),
+ HostName: C.GoString(cInfo.hostName),
+ PhysicalMemoryBytes: uint64(cInfo.physicalMemory),
+ SystemUptimeSeconds: float64(cInfo.systemUptime),
+ ProcessorCount: int(cInfo.processorCount),
+ ActiveProcessorCount: int(cInfo.activeProcessorCount),
+ IsMacCatalystApp: cInfo.isMacCatalystApp != 0,
+ IsIOSAppOnMac: cInfo.isiOSAppOnMac != 0,
+ IsIOS: cInfo.isIOS != 0,
+ UserName: C.GoString(cInfo.userName),
+ FullUserName: C.GoString(cInfo.fullUserName),
+ }
+
+ return goInfo, nil
+}
+
+// CstrToString converts a null-terminated []int8 byte slice to a string.
+func CstrToString(arr []byte) string {
+ buf := make([]byte, 0, len(arr))
+ for _, v := range arr {
+ if v == 0x00 {
+ break
+ }
+ buf = append(buf, byte(v))
+ }
+ return string(buf)
+}
+
+func main() {
+ fmt.Println("Attempting to get iOS process info using Cgo...")
+
+ // Call our Go wrapper function.
+ info, err := getProcessInfo()
+ if err != nil {
+ log.Fatalf("Error: %v", err)
+ }
+
+ osHostname, err := os.Hostname()
+ if err != nil {
+ osHostname = err.Error()
+ }
+
+ uts := new(unix.Utsname)
+ err = unix.Uname(uts)
+ if err != nil {
+ slog.Error("uname failed", "error", err)
+ }
+ // Print all the retrieved information in a formatted way.
+ fmt.Printf("\n--- Successfully Retrieved Process Info ---\n")
+ fmt.Printf("Process Name: %s\n", info.ProcessName)
+ fmt.Printf("Process ID (PID): %d\n", info.ProcessIdentifier)
+ fmt.Printf("User Name: %s\n", info.UserName)
+ fmt.Printf("Full User Name: %s\n", info.FullUserName)
+ fmt.Printf("Globally Unique ID: %s\n", info.GloballyUniqueString)
+ fmt.Printf("OS Version: %s\n", info.OperatingSystemVersionString)
+ fmt.Printf("Hostname: %s\n", info.HostName)
+ fmt.Printf("Is Mac Catalyst App: %t\n", info.IsMacCatalystApp)
+ fmt.Printf("Is iOS App on Mac: %t\n", info.IsIOSAppOnMac)
+ fmt.Printf("Is iOS: %t\n", info.IsIOS)
+ fmt.Printf("Physical Memory (B): %d\n", info.PhysicalMemoryBytes)
+ fmt.Printf("System Uptime (s): %.2f\n", info.SystemUptimeSeconds)
+ fmt.Printf("Processor Count: %d\n", info.ProcessorCount)
+ fmt.Printf("Active Processor Count: %d\n", info.ActiveProcessorCount)
+ fmt.Printf("os.Args: %s\n", os.Args)
+ fmt.Printf("os.Getpid(): %d\n", os.Getpid())
+ fmt.Printf("os.Hostname(): %s\n", osHostname)
+ fmt.Printf("unix.Uname:\n")
+ fmt.Printf(" Sysname: %s\n", CstrToString(uts.Sysname[:]))
+ fmt.Printf(" Nodename: %s\n", CstrToString(uts.Nodename[:]))
+ fmt.Printf(" Release: %s\n", CstrToString(uts.Release[:]))
+ fmt.Printf(" Version: %s\n", CstrToString(uts.Version[:]))
+ fmt.Printf(" Machine: %s\n", CstrToString(uts.Machine[:]))
+ fmt.Println("-------------------------------------------")
+}