Problem with enums in embedded swift

I play with pure bare metal example for esp32c6 Bare-Metal Swift on M5Stack NanoC6: No C, No Assembly, No ESP-IDF - #3 by trickart from @trickart

I tried to make PWMLed and use enums:

struct PWMLedOld {
enum Timer: UInt32 {
        case timer0 = 0
        case timer1
        case timer2
        case timer3
        case timerMax
    }

    enum Channel: UInt32 {
        case channel0 = 0
        case channel1
        case channel2
        case channel3
        case channel4
        case channel5
        case channel6
        case channel7
        case max
    }
    ......
    let pin: Int
    let channel: PWMLed.Channel
    let timer: PWMLed.Timer
    let mode: PWMLed.Mode
    let maxDuty: UInt32

    init?(
        pin: Int,
        channel: PWMLed.Channel = .channel0,
        timer: PWMLed.Timer = .timer0,
        frequencyHz: UInt = 1000,
        mode: PWMLed.Mode = .low,
        dutyResolution: PWMLed.TimerBit = .bit13,
    ) {

etc. to have safe nice swifty code. But completely failed.

It's enough tu use rawValue once.

func setDuty(_ duty: UInt32) {
        let base = UInt32(CurrentBoard.DeviceAddress.LEDC)
        let ch = UInt32(channel.rawValue) * 0x14 // <- here
        regStore(base + ch + 0x08, duty << 4)
        regStore(base + ch + 0x0C, (1 << 31))
        regStore(base + ch + 0x00, (1 << 2) | (1 << 4))
    }

It compiles and it fails right after:

TOOLCHAINS=org.swift.630202603201a swift build --triple riscv32-none-none-eabi --toolset toolset.json --product Application
Building for debugging...
error: link command failed with exit code 1 (use -v to see invocation)
clang: warning: argument unused during compilation: '-F/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks' [-Wunused-command-line-argument]
ld.lld: error: undefined symbol: arc4random_buf
>>> referenced by <compiler-generated>:0 (/<compiler-generated>:0)
>>>               /Users/lukasz/Swift-Exprymenty/BareMetalESP32/esp32c6-bare-experiment-ledc/.build/riscv32-none-none-eabi/debug/Application.build/PWMLed.swift.o:($es32_swift_stdlib_Hashing_parameters_WZ)
>>> referenced by <compiler-generated>:0 (/<compiler-generated>:0)
>>>               /Users/lukasz/Swift-Exprymenty/BareMetalESP32/esp32c6-bare-experiment-ledc/.build/riscv32-none-none-eabi/debug/Application.build/PWMLed.swift.o:($es32_swift_stdlib_Hashing_parameters_WZ)

enums doesn't work at all. If I use Int32 it works perfect

struct PWMLed {
   let pin: Int
   let channel: UInt32
    let timer: UInt32
    let mode: UInt32
    let maxDuty: UInt32

   init?(
        pin: Int,
        channel: UInt32 = 0, 
        timer: UInt32  = 0, 
        frequencyHz: UInt = 1000,
        mode: UInt32 = 1, 
        dutyResolution: UInt32 = 13 
    ) {
        self.pin = pin
        self.channel = channel
        self.timer = timer
        self.mode = mode
        self.maxDuty = (1 << dutyResolution) - 1

        configTimer()
        configChannel()
    }

in such a bare metal way enums seems to not work at all. Or they somehow does?

I'm not exactly sure why you see the arc4random dependency, but it is documented here: External Dependencies.

  • using Hashable, Set, Dictionary, or random-number generating APIs
    • dependency: void arc4random_buf(void *, size_t);

cc @Douglas_Gregor @Max_Desiatov

Enums without payloads implicitly conform to Hashable for historical reasons. :-( Removing this has been brought up before, but hasn't made it to an evolution proposal yet.

4 Likes

Might be risky in some cases, but I’ve done this in Embedded to satisfy the linker:

@_cdecl("arc4random_buf")
func arc4random_buf(_ buf: UnsafeMutableRawPointer, _ size: Int) {
  // fatalError, while true {}, ...
  // or write your own routine
}

I’ve never had a problem with it (hangs), but I definitely wouldn’t say this is safe/best practice.

2 Likes

I'm mostly surprised this isn't dead stripped before we get to the linker. Many of the other types involved are also hashable

2 Likes

I think I have a talent to find broken things. How to deal with it? Simply wait?

I would implement arc4random with a fatalError as suggested to avoid the linker error as a workaround. I'm curious if any code path actually calls it at runtime.

1 Like

Not that I’ve seen, and I was trying it on a large-ish project (kernel). Granted I do use -no-allocations so you’re not even allowed to create Sets/Dictionaries that will need the arc4. Maybe he will hit fatalError in that proj :slight_smile:

I’ll see if adding anything (and switching off no-alloc) ever finds a call to it.

Thanks, @_cdecl("arc4random_buf") solved problem.
Also I had to add:

@_cdecl("__ashldi3")
func __ashldi3(_ val: UInt64, _ shift: Int32) -> UInt64 {
    return val << shift
}

@_cdecl("__lshrdi3")
func __lshrdi3(_ val: UInt64, _ shift: Int32) -> UInt64 {
    return val >> shift
}

to fully enjoy swift enums.

Note: you should be able to use @c for this. @_cdecl is a legacy spelling.

4 Likes

I wonder the same.

Is this really called?

func __ashldi3(_ val: UInt64, _ shift: Int32) -> UInt64 {
    return val << shift
}

And if it is called won't it cause an infinite recursion?

I'd put fatalError to each and every of those symbols to know for sure "no, there are not getting called in my use cases, just there to satisfy the linker" or "yes they are called, and then have to implement them properly".

Yeah, that will infinitely recurse - took way too long to set up, but I have a repro now…

Most of these intrinsics are for unsupported instructions around specific bit-widths, and require some tricks to lower to smaller widths. For example, LLVM’s compiler-rt's intrinsics still use shifts.

The original repo is targeting RISCV and does mention no assembly and C; but if you’re fine with breaking that promise for peace of mind:

rv32: \__ashldi3
__ashldi3:
  li t0, 32
  blt a2, t0, 1f
  sub a2, a2, t0
  sll a1, a0, a2
  li a0, 0
  ret
1:
  beqz a2, 2f
  sub t0, t0, a2
  sll a1, a1, a2
  srl t1, a0, t0
  or a1, a1, t1
  sll a0, a0, a2
2:
  ret

I’ve since found (after investigating all of ^, and making my asm redundant) the original repo has helpers for these (docs, in swift), so I’m really curious why OP had to manually add the intrinsics… but I’m not too familiar with both the repo setup and OPs setup.

Hi! Thanks for playing around with my project!

The ESP32-C6's RISC-V (RV32IMC) is a 32-bit architecture, so it lacks native 64-bit shift instructions. Additionally, Embedded Swift doesn't link the standard library or compiler-rt, so I needed to provide our own implementations.

1 Like

Yes I know :) I meant, since you already had them added, why did @Typoland have to add them back in.

I added them after @Typoland encountered the issue. :)

1 Like

I learned a lot. Thanks!

Playing with bare metal, mmio etc.. season 2 episode 1.
I updated packages and new clang error: __udivdi3 not found this time.

ld.lld: error: undefined symbol: __udivdi3
>>> referenced by <compiler-generated>:0 (/<compiler-generated>:0)
>>>               /Users/lukasz/Swift-Exprymenty/BareMetalESP32/esp32c6-bare-experiment-ledc/.build/riscv32-none-none-eabi/debug/Application.build/Application.swift.o:($es21_BinaryIntegerToASCII8negative9magnitude5radix9uppercase6bufferSnySiGSb_9MagnitudeQzxSbs11MutableSpanVys5UInt8VGztSzRzlFs6UInt64V_Tg5)
>>> referenced by <compiler-generated>:0 (/<compiler-generated>:0)
>>>               /Users/lukasz/Swift-Exprymenty/BareMetalESP32/esp32c6-bare-experiment-ledc/.build/riscv32-none-none-eabi/debug/Application.build/Application.swift.o:($es21_BinaryIntegerToASCII8negative9magnitude5radix9uppercase6bufferSnySiGSb_9MagnitudeQzxSbs11MutableSpanVys5UInt8VGztSzRzlFs6UInt64V_Tg5)

I suspect it is bug, maybe...? Should I implement @_cdecl("__udivdi3")?

@rauhul: @(c) somehow does not work with main-snapshot-2026-03-16 (in use). I don't know why, maybe I don't understand something.

My understanding is that these intrinsics should come from libcompilerrt, but beyond that I dont have a ton of expertise.

Right. I'm not sure what in this code is using the implicit Hashable conformance (just pasting the code from the original post didn't trigger it for me), but you can trigger the arc4random_buf dependency by using the Hashable conformance:

public enum Timer: UInt32 {
case timer0 = 0
case timer1
case timer2
case timer3
case timerMax
}

public func doIt() {
  let _: any Hashable = Timer.timer1
}

At the very least, we should provide a way to suppress the implicit conformance, as noted here.

Doug

1 Like

Would overriding hashValue get rid of the linker error in this case?

enum ... Hashable {
    ...
    var hashValue: Int { 0 }
}