Skip to content

Reimplement the Linux HAL in native Swift using @c#2

Merged
lhoward merged 2 commits into
mainfrom
native-swift-hal
Jul 20, 2026
Merged

Reimplement the Linux HAL in native Swift using @c#2
lhoward merged 2 commits into
mainfrom
native-swift-hal

Conversation

@lhoward

@lhoward lhoward commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Migrates the entire swifthal_* HAL from ~2,900 lines of hand-written C to native Swift, exporting the identical C ABI via the Swift 6.3 @c attribute (SE-0495). SwiftIO/AsyncSwiftIO link against it unchanged — no caller sees the language change.

Structure

  • The single C target is split into CLinuxHalSwiftIO (residual C) and a new Swift target LinuxHalSwiftIO (the public product). The Swift target @_exported imports the C module, so import LinuxHalSwiftIO still surfaces the HAL declarations to consumers.
  • Every subsystem is now Swift: spi, i2c, uart, gpio, os, platform, fs, timer, counter, and the adc/eth/i2s/lcd/pwm/wifi stubs.
  • gpio and timer use the Swift Dispatch overlay (no libdispatch/Blocks C runtime); gpio drives libgpiod directly. The interrupt "block" entry point — consumed only by AsyncSwiftIO — becomes a native Swift closure API instead of an Objective-C block.

Minimal C that remains

Things Swift genuinely cannot express, named with the existing swifthal_<subsys>__<helper> internal convention:

  • swift_platform_hwcycle.chwcycle_get/hwcycle_to_ns (inline assembly).
  • swift_uart_native.c — termios2 line configuration (asm/termbits.h collides with Glibc's termios.h, so it can't be exposed to Swift).
  • swift_hal_native.hstatic inline wrappers for ioctl request-number macros (SPI/I2C), variadic mq_open, syslog, plus SPI mode-bit constants.

Pre-existing bugs fixed along the way

  • os mq_recv tested != 0, but mq_receive returns a byte count, so it returned -errno on every successful receive; corrected to < 0.
  • The RNG was defined as swiftHal_randomGet, which never matched the declared swifthal_random_get symbol and so could not be linked; renamed to match the ABI.
  • The SPI loopback test built SPI in a stored property, crashing test discovery on any machine without SPI hardware; made it lazy.

Toolchain

CI bumped swift:6.2swift:6.3 (@c requires 6.3).

Verification

  • swift build (library, examples, tests) is green.
  • Symbol audit: each swifthal_* symbol is defined exactly once — Swift for logic, C only for hwcycle + termios2 helpers; no duplicates.
  • HALNativeTests (semaphore limit enforcement, mutex, RNG fill, uptime) pass; an mq round-trip / sleep smoke check passed during development. The SPI loopback test still requires real hardware.

Post-review fixes

A multi-agent code review of the branch produced eight confirmed findings. Five were migration regressions, fixed in 31af742:

  • uart blocking read (timeout == -1) looped forever instead of returning after one readBuffer pass, as the C did.
  • spi transceive used a trapping UInt32(...) on the transfer length; uart likewise trapped converting nremain to CInt. Both now truncate, matching C.
  • The wifi and lcd stubs stopped zeroing their out-params before returning -ENOSYS, leaving callers reading stack garbage.

Two further findings were faithful ports of the original C behavior that were nonetheless worth improving, done separately in e58e298:

  • fs readdir aborted the whole enumeration when a DT_REG entry lost a race with unlink; it now skips the entry, matching the DT_UNKNOWN path.
  • gpio interrupt_callback_uninstall only cancelled the dispatch source, so a later interrupt_enable silently resumed a permanently-cancelled source; it now releases it, and the later call returns -EINVAL.

One finding is deliberately unaddressed: fs_write/fs_read truncate a single transfer larger than 2 GiB. That matches the original (int)nbytes and is not a realistic single-call scenario, so it stays a pure port.

🤖 Generated with Claude Code

@lhoward lhoward self-assigned this Jul 19, 2026
@lhoward lhoward added the enhancement New feature or request label Jul 19, 2026
@lhoward
lhoward force-pushed the native-swift-hal branch from e58e298 to f72f731 Compare July 20, 2026 00:20
Migrate the entire swifthal_* HAL from ~2,900 lines of C to native Swift,
exporting the identical C ABI via the Swift 6.3 @c attribute (SE-0495) so
SwiftIO/AsyncSwiftIO link against it unchanged.

Structure
- Split the single C target into CLinuxHalSwiftIO (residual C) and a new Swift
  target LinuxHalSwiftIO (the public product), which @_exported imports the C
  module so `import LinuxHalSwiftIO` still surfaces the HAL declarations.
- All subsystems are now Swift: spi, i2c, uart, gpio, os, platform, fs, timer,
  counter, and the adc/eth/i2s/lcd/pwm/wifi stubs.
- gpio and timer use the Swift Dispatch overlay (no libdispatch/Blocks C
  runtime); gpio drives libgpiod directly. The interrupt block entry point,
  consumed only by AsyncSwiftIO, is now a native Swift closure API rather than
  an Objective-C block.

Minimal C that remains (things Swift cannot express), named with the existing
`swifthal_<subsys>__<helper>` internal convention:
- swift_platform_hwcycle.c: hwcycle_get/to_ns (inline assembly).
- swift_uart_native.c: termios2 line configuration (asm/termbits.h conflicts
  with Glibc's termios.h, so it cannot be exposed to Swift).
- swift_hal_native.h: static-inline wrappers for ioctl request-number macros
  (SPI/I2C), variadic mq_open, and syslog, plus SPI mode-bit constants.

Pre-existing bugs fixed along the way
- os mq_recv tested `!= 0`, but mq_receive returns a byte count, so it returned
  -errno on every successful receive; corrected to `< 0`.
- The RNG was defined as swiftHal_randomGet, which never matched the declared
  swifthal_random_get symbol and so could not be linked; renamed to match.
- The SPI loopback test built SPI in a stored property, crashing test discovery
  on any machine without SPI hardware; made it lazy.

Toolchain: CI bumped swift:6.2 -> swift:6.3 (@c requires 6.3).

Verification: swift build (library, examples, tests) is green; a symbol audit
confirms each swifthal_* symbol is defined exactly once (Swift for logic, C only
for hwcycle + termios2); HALNativeTests (semaphore limit, mutex, RNG, uptime)
pass, and an mq round-trip / sleep smoke check passed during development.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ps1JiW8Nh8mbFERFeR9DV
@lhoward
lhoward force-pushed the native-swift-hal branch from f72f731 to 5f86dff Compare July 20, 2026 01:00
- Factor the dispatch-source suspend/resume/teardown patterns shared by
  GPIO and Timer into a DispatchSourceProtocol extension, and add
  handle() unwrappers instead of repeated Unmanaged boilerplate
- GPIO: release the source fully in the event-handler error path rather
  than leaving a cancelled-but-installed source behind
- FS: factor the dirent type/size fill shared by readdir and fs_stat;
  DT_UNKNOWN entries are now stat'ed once instead of twice
- Counter: implement tick conversions as microseconds (1 MHz) instead of
  stubbing them to zero, making counter_read meaningful
- Stubs: replace the memset spelling of LCD format zeroing with a plain
  pointee assignment
- Correct stale comments (swift_uart_native.h prototypes, CLinuxHalSwiftIO
  target contents) and document the x86 TSC calibration cost

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rhut2AwGnktQ4msgGo6z1
@lhoward
lhoward force-pushed the native-swift-hal branch from dfe35cf to 1be9c83 Compare July 20, 2026 01:20
@lhoward
lhoward merged commit 344c7b0 into main Jul 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant