Double value cannot be converted to UInt8 because the result would be less than UInt8.min

I have a swift callback function that accumulates data from the gyroscope handler CMGyroData to value, and takes care that it never goes below zero. Any thoughts why I got this crash?

var value: Double = 90

func test(d: Double)
{
	value -= d

	if value < 0
	{
		value = 0
	}
	else if value > 180
	{
		value = 180
	}

	// Double value cannot be converted to UInt8 because the result would be less than UInt8.min
	// according to Xcode, value is 0
	let angle = UInt8(value.rounded())
	print(angle)
}

During the crash, the debugger shows that value is 0, and the raw memory is 0x0000000000000000. I've heard about negative zero, but then the raw memory would be 0x8000000000000000. Either way, it makes no sense for the UInt8 constructor to fail.

(lldb) p value
(Double) 0
(lldb) p value.rounded()
(Double) 0
(lldb) p UInt8(value.rounded())
(UInt8) 0

Xcode 15.1, iPhone 7 Plus, iOS 15.8

Edit: As a good measure, I changed that line to

let angle = UInt8(Int(value.rounded()))

Replies

If you're trying to initialize an unsigned Swift integer from something that might be negative, Swift won't let you unless you're explicit.

let x:Int8 = -1
let y = UInt8(x)

fails because "negative value is not representable" but this works

let y = UInt8(bitPattern: x) 

y will be 255

Thanks, that's why there is a check: if value is negative, I set it to zero. The only case in which this could crash is if two instances of my function are ran concurrently, which does happen during heavy load, CMMotionManager seems to call my handler again before the previous instance returns. Even though there is little code after the check, the second instance could set value to negative, which would produce the crash. Then it was able to change it back to zero right before the debugger stopped the application. That last part got me confused. The solution was to use a local variable, that is checked and corrected. Moral of the story: always work with local copies of anything that should not be changed externally. Cheers!

The only case in which this could crash is if two instances of my function are ran concurrently, which does happen during heavy load

Well done for debugging that.

The traditional fix would be a mutex that protects value. I guess that the "modern Swift / Apple way" to do this would involve a serial queue.

Moral of the story: always work with local copies of anything that should not be changed externally.

That's not necessarily sufficient for correctness.

The traditional fix would be a mutex that protects value. That's not necessarily sufficient for correctness.

Indeed for correctness it is not. In C I would do atomic sub and fetch. There's also the question if it is worth switching to kernel space to lock a mutex on code that runs frequently and would not cause any noticeable side effects. In this case I would say no.

I guess that the "modern Swift / Apple way" to do this would involve a serial queue.

I just learned how to use Swift queues yesterday. 😊 Coming from C/C++ it took a day to bend my mind and get things running smoothly. Namely replacing a cached Path object in use by SwiftUI from a worker thread is a good way to trigger double free. Memory management in Swift might use some improvement. They relay on the assumption that you do everything on the main thread. At my opinion, queues are an easy way to serialise activities or run a group of things concurrently. I wonder how efficient they are? First there is code to manage the queue, and then if an operation has to wait, a context switch is required to process another. Or does the whole queue stall? I've seen projects in Zephyr OS, where they create objects to pass data between tasks. It's handled in serialised work queues. And it's horribly slow, complex and prone to errors. When I designed nano RTOS, I figured I can make IO calls somewhere in between synchronous and asynchronous. In theory the calls might block, in practice they don't. The user can write simple linear code: receive, process, send all in one task. It does not drop data. The OS takes care of the rest.

One more observation:

always work with local copies of anything that should not be changed externally.

I am surprised that this isn't what happens in the compiled code anyway. I.e. given your source code

v -= d;
if (v < 0) v = 0;
if (v > 180) v = 180;
f(v);

I would expect the compiler to generate code that keeps v in a register through all of that and not read and write to the global variable location, i.e. in pseudo-assmbler:

reg = load(v)
reg = reg - d
compare reg, 0
if less reg = 0
compare reg, 180
if greater reg = 180
store reg to v
move from reg to function arg location (if different)
call f

In that case, there is no opportunity for the concurrent invocation of the function to modify the value of v between the clamping tests and the subsequent use.

Maybe you have compiled with no optimisation? Maybe your real code is more complicated than what you've posted?

Anyway, I suggest that you look at the generated code (if you can work out how to do that) and see if it is really doing what you think it is doing.

And one final comment:

How are you actually getting this gyro data?

https://developer.apple.com/documentation/coremotion/cmmotionmanager/1616104-startgyroupdatestoqueue

That takes a queue on which it will invoke the callback. If you pass a serial queue there, you should not get concurrent calls.

I am surprised that this isn't what happens in the compiled code anyway. I.e. given your source code I would expect the compiler to generate code that keeps v in a register through all of that and not read and write to the global variable location Anyway, I suggest that you look at the generated code (if you can work out how to do that) and see if it is really doing what you think it is doing.

View disassembly would be my first thing on a development board 👍🏻 Yes, in C/C++ it is very easy to control if the data will be kept in a register or loaded again. I wonder if Swift can be told to do so? Considering value is actually cnc.data.controller_gyro.value, a plain variable three ObservableObject classes deep (code below). And there are a couple of quick if checks, so the compiler may choose to load value again.

Maybe you have compiled with no optimisation? Maybe your real code is more complicated than what you've posted?

By default the iPhone run target is Debug, which should disable optimisations. I tried Release as well, and looked into the disassembly in both cases, but it is very hard for me to follow what ASM correspond to what Swift line. I usually have them side by side when debugging embedded boards, can I do this with Xcode? I get the impression value is loaded again. I write and debug Cortex-M ASM in kernel mode, but Swift seems to generate a lot of code for simple operations. If you wish I can post a screenshot?

That takes a queue on which it will invoke the callback. If you pass a serial queue there, you should not get concurrent calls.

In that case I should change my code to this

let queue = OperationQueue()
queue.name = "gyroscope"
queue.maxConcurrentOperationCount = 1
queue.qualityOfService = .userInteractive
motion.startGyroUpdates(to: queue, withHandler: gyro_handler)

How are you actually getting this gyro data?

Original implementation

class controller_gyro_t: ObservableObject
{
	@Published var active: Bool = false
	let start: TimeInterval = 0.20
	let delay: TimeInterval = 0.045
	let fast: TimeInterval = 0.00
	var last: TimeInterval = 0
	var timestamp: TimeInterval = 0
	var value: Double = 0
	var angle: UInt8 = 0
	var ack: Bool = false

	func stop()
	{
		if active
		{
			active = false
		}

		angle = 0
		value = 0
	}
}
struct ContentView: View
{
// ...
#if !os(macOS)
	let motion = CMMotionManager()
#endif

	func gyro_handler(data: CMGyroData?, err: Error?)
	{
		if cnc.data.stop || cnc.data.return_home
		{
			DispatchQueue.main.async
			{
				stop_controller()
			}

			return
		}

		if !cnc.data.controller_gyro.active
		{
			DispatchQueue.main.async
			{
				stop_controller_gyro()
			}

			return
		}

		if let gyro = data
		{
			let now = gyro.timestamp
			let gyro_dt = now - cnc.data.controller_gyro.last
			cnc.data.controller_gyro.last = now

			if gyro_dt > cnc.data.controller_gyro.start
			{
				// the last packet was very long ago
				// usually the first sample when we start receiving updates
				return
			}

			// rotation rate: rad/s
			var value = cnc.data.controller_gyro.value - gyro.rotationRate.z * gyro_dt * angle_rad_to_deg

			if value < angle_min
			{
				value = angle_min
			}
			else if value > angle_max
			{
				value = angle_max
			}

			cnc.data.controller_gyro.value = value

			if cnc.data.controller_gyro.ack
			{
				cnc.data.controller_gyro.ack = false
				cnc.data.controller_gyro.timestamp -= cnc.data.controller_gyro.fast
			}

			if cnc.data.controller_gyro.timestamp > now
			{
				return
			}

			let angle = UInt8(value.rounded())

			if cnc.data.controller_gyro.angle != angle
			{
				cnc.data.controller_gyro.angle = angle
				cnc.data.controller_gyro.timestamp = now.advanced(by: cnc.data.controller_gyro.delay)
				let a = "a\(angle)"
				send_no_log(a)

				DispatchQueue.main.async
				{
					cnc.data.angle = Float(angle)
				}
			}
		}
	}

	func btn_gyro_control() -> some View
	{
		Button
		{
			haptic_feedback()

			if cnc.data.controller_gyro.active
			{
				stop_controller()
				send_cmd(cmd_stop)
			}
			else
			{
				stop_controller()
				cnc.data.stop = false
				cnc.data.return_home = false
				cnc.data.controller_gyro.active = true
				cnc.data.controller_gyro.value = angle_mid
				cnc.data.controller_gyro.angle = UInt8(angle_min)

				send_cmd("u")

#if !os(macOS)
				if motion.isGyroAvailable
				{
					DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + cnc.data.controller_gyro.start)
					{
						motion.startGyroUpdates(to: OperationQueue(), withHandler: gyro_handler)
					}
				}
#endif
			}
		} label:
		{
			Image(systemName: cnc.data.controller_gyro.active ? "gyroscope" : "gyroscope")
				.button_style_image(
					cnc.connected ? cnc.data.controller_gyro.active ? colour_store : colour_btn : colour_disabled,
					fg: cnc.connected ? .white : colour_btn_fg_disabled
				)
		}
		.font(.system(size: size_font_img, design: Font.Design.rounded))
		.keyboardShortcut("u", modifiers: [.command])
	}
// ...
}

PS: Thank you for your brainstorming ideas 😊 And sorry to the late replay. I had some issues porting the Mac Catalyst version of the app to Mac. Broken Slider and keyboardShortcut to name a few. Hint: the following code works well on iPhone and Mac Catalyst. Try Mac. Hint: play with step or without.

@State var inclination: Double = 0
//...
Slider(value: $inclination, in: -80...80, step: 0.01)
Button("press option+1"){ print("option+1") }.keyboardShortcut("1", modifiers: [.option])
Button("¡surprise!"){ print("¡surprise!") }.keyboardShortcut("¡", modifiers: [.option])