Creating a Swift type for button input on nRF52 - Part 2
Introduction
In the last post, Creating a Swift type for button input on nRF52 - Part 1, we worked on code to abstract interacting with a button in Swift code. We had a working prototype, but things fell apart as soon as we cleaned up the code and encapsulated it in a struct.
Investigating the issue
Let’s see if there’s valuable information in the documentation.
/**
* @brief GPIO callback structure
*
* Used to register a callback in the driver instance callback list.
* As many callbacks as needed can be added as long as each of them
* are unique pointers of struct gpio_callback.
* Beware such structure should not be allocated on stack.
*
* Note: To help setting it, see gpio_init_callback() below
*/
struct gpio_callback {
/** This is meant to be used in the driver and the user should not
* mess with it (see drivers/gpio/gpio_utils.h)
*/
sys_snode_t node;
/** Actual callback function being called when relevant. */
gpio_callback_handler_t handler;
/** A mask of pins the callback is interested in, if 0 the callback
* will never be called. Such pin_mask can be modified whenever
* necessary by the owner, and thus will affect the handler being
* called or not. The selected pins must be configured to trigger
* an interrupt.
*/
gpio_port_pins_t pin_mask;
};
The documentation for the definition of the gpio_callback
gives an interesting piece of information.
Can you spot it?
Beware such structure should not be allocated on stack.
Let’s look at our Swift code again
struct Button {
var pin_cb_data = gpio_callback()
...
Here we’re creating a gpio_callback
as a stored property of a Swift struct. That struct being a value type, it will be allocated on the stack, which the above comment explicitly advises against.
An easy fix is to make the Button a class, which as a reference type, will be allocated on the heap.
Info
The idea that value types are always created on the stack and reference types on the heap is not entirely true. I have not yet been able to find an exact rule but the approximation works for now and for this particular case.
A safer way (which would also allow to keep our Button as a struct) is to explicitly allocate the gpio_callback
structure on the heap. This however opens a can of worms that is too much for this article. But it’s a topic we’ll revisit later and in great details.
Beyond printing a message
The code is now working again, but all it does is print a message when the button is pressed. Let’s move to something slightly more interesting and, re-using code from our first example (Controlling an LED using Embedded Swift on nRF52), make the button toggle an LED.
We’ll use a simplified version of the Led code, omitting the error handling and not throwing any errors, as to make things more readable.
We can then update the closure passed to the button so it toggles the LED when the button is pressed.
struct Main {
static func main() {
let led = Led(gpio: &led0)
let myButton = Button(gpio: &button) { _, _, _ in
led.toggle()
}
...
But this makes the build fail
…/EmbeddedSwift-nRF52-Examples/Button-and-LED/Main.swift:52:42: error: a C function pointer cannot be formed from a closure that captures context
50 | let led = Led(gpio: &led0)
51 |
52 | let myButton = Button(gpio: &button) { _, _, _ in
| `- error: a C function pointer cannot be formed from a closure that captures context
53 | led.toggle()
54 | }
As the error message indicates, the closure cannot capture references to variables from its context.
We have two main possibilities to fix this:
- make the variable that we want to reference globally available
- somehow pass the reference to the C function
Let’s explore these 2 options in more details.
Making our led variable globally available
There are multiple ways to achieve this.
Global variable
The first and easiest way is to just make it a global variable for the module.
Moving the let led = Led(gpio: &led0)
declaration outside of the Main
struct makes it available from within the closure.
The code would look something like this
let led = Led(gpio: &led0)
@main
struct Main {
static func main() {
let myButton = Button(gpio: &button) { _, _, _ in
led.toggle()
}
...
This builds and works fine, and is OK for simple cases but it’s definitely not the best code organisation and does not scale very well.
Static variable on a type
Instead, we can define a static constant property on the existing Led
type
struct Led {
static let firstLed = Led(gpio: &led0)
...
and refer to it from the closure
let myButton = Button(gpio: &button) { _, _, _ in
Led.firstLed.toggle()
}
If you don’t want to modify the Led
type and keep it generic (for instance because it’s coming from an external package), you can create a dedicated type (an enum in this case) to hold the static constants.
enum Leds {
static let firstLed = Led(gpio: &led0)
}
let myButton = Button(gpio: &button) { _, _, _ in
Leds.firstLed.toggle()
}
Application logic singleton
In all the above examples, we’ve used different strategies to allow global access to a single definition of the Led
constant.
If we would have multiple elements to expose, or we would want to encapsulate those elements behind some more advanced logic, we can create a class to host that logic and make it a singleton.
The singleton pattern ensures that the class would only have one instance and provides a global access point to that instance.
The code could look something like this.
class AppLogic {
static let shared = AppLogic()
private init() { }
let firstLed = Led(gpio: &led0)
func toggleLed() {
firstLed.toggle()
}
}
With the button closure using the logic exposed by the singleton.
let myButton = Button(gpio: &button) { _, _, _ in
AppState.shared.toggleLed()
}
Info
In the above implementation of the singleton, the AppLogic
instance is only created when the shared static property is first accessed. And so is the initialisation of our Led
instance. This means this only happens on the first button press.
If this is not acceptable for some reason, you could always read the shared property in your main function to trigger the instantiation earlier.
Passing the reference to the C function
Exploring the possibilities
The general idea is that the required information comes via the parameter(s) of the C function (closure) that’s being called. As we’re implementing a callback whose prototype is defined by the Zephyr SDK, we cannot add the parameter ourself.
But is there already something in the API that we can use? Let’s take another look at the callback prototype.
/**
* @typedef gpio_callback_handler_t
* @brief Define the application callback handler function signature
*
* @param port Device struct for the GPIO device.
* @param cb Original struct gpio_callback owning this handler
* @param pins Mask of pins that triggers the callback handler
*
* Note: cb pointer can be used to retrieve private data through
* CONTAINER_OF() if original struct gpio_callback is stored in
* another private structure.
*/
typedef void (*gpio_callback_handler_t)(const struct device *port,
struct gpio_callback *cb,
gpio_port_pins_t pins);
Do you see the note in the documentation ? The API has foreseen a way to pass private data to this handler. Let’s try to figure out how that works.
Searching the Zephyr documentation for CONTAINER_OF
, we find some information in Zephyr API Documentation: Utility Functions. The basic idea is that given an element of a struct, we’re able to get back to the containing struct.
Let’s look at the example from the documentation
struct foo {
int bar;
};
struct foo my_foo;
int *ptr = &my_foo.bar;
struct foo *container = CONTAINER_OF(ptr, struct foo, bar);
Here we see that, knowing the address of bar
, an element of my_foo
, the CONTAINER_OF
macro will return the address of that my_foo
struct.
If the struct foo
would contain a struct gpio_callback
instead of an int
, that would read
struct foo {
struct gpio_callback cb;
};
struct foo my_foo;
int *ptr = &my_foo.cb;
struct foo *container = CONTAINER_OF(ptr, struct foo, cb);
We’re getting to a containing structure from the gpio_callback
element, which is one of the parameters we receive in our GPIO handler function.
Understanding CONTAINER_OF
Looking at the definition of CONTAINER_OF, we see it’s a “complex” macro
#define CONTAINER_OF(ptr, type, field) \
({ \
CONTAINER_OF_VALIDATE(ptr, type, field) \
((type *)(((char *)(ptr)) - offsetof(type, field))); \
})
and we’ve seen in the previous post that Swift C Interop only supports simple macros.
Instead of trying to see how we could call this macro from Swift, let’s understand what it does and re-implement the same behaviour using Swift native constructs.
Setting aside some validation, the macro implements some basic pointer arithmetics. Considering the containing struct is represented by a contiguous block of memory, knowing the address of one of its property and the offset of that property from the start, by subtracting that offset from the address we get back the starting address of the memory block i.e. the address of the containing structure.
Let’s extend our foo
structure with some int property, create a variable of that type and see how the maths work
struct foo {
int bar;
struct gpio_callback cb;
};
struct foo my_foo;
int *ptr = &my_foo.cb;
Let’s pretend my_foo
is at address 0x00001000. The addresses of the structure look like
Address | Element |
---|---|
0x00001000 | bar |
0x00001004 | cb |
ptr
is the address of cb
and would have value 0x00001004. offsetof(struct foo, cb)
would return 4. Subtracting that from ptr
gives 0x00001000, which is indeed the address of my_foo
.
Pointer arithmetics in Swift
Swift provides several types for Manual Memory Management, you can spot them by their name following the Unsafe…Pointer
pattern.
Of the different flavours available, UnsafeRawPointer
is one of the simplest, providing untyped access to raw bytes in memory. It corresponds to a void *
pointer in C.
Raw pointers support basic arithmetic operations at the byte level. When you add to or subtract from a raw pointer, the result is a new raw pointer offset by that number of bytes.
Using UnsafeRawPointer
will allow us to implement the above subtraction from our CONTAINER_OF formula.
What about the offsetof()
call ? This is an ANSI C macro that evaluates to the offset (in bytes) of a given member within a struct or union type.
It happens that Swift provides the same functionality with the offset(of:) function of the MemoryLayout type.
Finally, once done with the pointer arithmetic, we would like to “cast” the result to the type of our container structure. We can achieve that using the assumingMemoryBound(to:) on our UnsafeRawPointer
, that will return us an UnsafePointer
typed to the requested type.
Putting it all together
In our project, we wanted the Led
to be available with the button handler closure. We thus create a container structure, that in addition to the original callback structure, contains our Led
.
struct ExtendedCallback {
var callback: gpio_callback
var led: Led
}
We then add, on that structure, a function to perform the pointer arithmetic, and return, given a pointer to the callback
element, the ExtendedCallback
.
struct ExtendedCallback {
var callback: gpio_callback
var led: Led
static func containerFrom(callback ptr: UnsafePointer<gpio_callback>) -> ExtendedCallback {
let containerPtr = UnsafeRawPointer(ptr) - MemoryLayout.offset(of: \Self.callback)!
return containerPtr.assumingMemoryBound(to: Self.self).pointee
}
}
With that in place, we can now access the Led
from within the button closure.
let myButton = Button(gpio: &button) { _, callback, _ in
ExtendedCallback.containerFrom(callback: callback!).led.toggle()
}
Of course, we need to properly initialise this property. We edit our Button
class so that, the pin_cb_data
property is initialized with an ExtendedCallback
, encapsulating both our initial gpio_callback
and our Led
.
var pin_cb_data = ExtendedCallback(callback: gpio_callback(), led: Led(gpio: &led0))
Within our initializer, we need to adapt the calls that are taking the gpio_callback
as parameters, as we still need to pass that original structure and not the container.
gpio_init_callback(&pin_cb_data.callback, self.btnHandler, bit(gpio.pointee.pin))
gpio_add_callback(gpio.pointee.port, &pin_cb_data.callback)
With that in place, we can build the project and test that indeed, the LED toggles as the user pushes the button.
We’ve been able to use this strategy here because the API was designed in such a way that some object accessible to us during initialization, was propagated to the callback. This will not always be the case and you’ll need to adapt based on the specific use case.
Making it generic
There are 2 shortcomings to the above code:
- we have hardcoded in the
ExtendedCallback
structure that the extra information is aLed
- that
Led
is instantiated as part of theButton
code
Let’s use generics to get rid of those limitations.
Info
Embedded Swift supports generics but uses specialisation i.e. the generic type is known at compile-time at the caller site and the compiler creates a specialized instantiation of the generic type that is no longer generic.
Generic instantiation at run-time is not supported, as that would require access to type metadata, which does not exist in Embedded Swift.
We start by making the extra parameter generic
struct ExtendedCallback<T> {
var callback: gpio_callback
var context: T
...
We also need to modify the Button
class in accordance
class Button<T> {
let gpio: UnsafePointer<gpio_dt_spec>
let btnHandler: GpioCallbackHandler
var pin_cb_data: ExtendedCallback<T>
init(gpio: UnsafePointer<gpio_dt_spec>, context: T, btnHandler: GpioCallbackHandler) {
self.gpio = gpio
self.btnHandler = btnHandler
self.pin_cb_data = ExtendedCallback(callback: gpio_callback(), context: context)
...
We make the class generic, the pin_cb_data
is now the generic version of ExtendedCallback
and that property is initialized in the initializer, receiving the context as a parameter.
The closure needs a slight update, as ExtendedCallback
must be specialized for us to call its the static function.
let myButton = Button<Led>(gpio: &button, context: Led(gpio: &led0)) { _, callback, _ in
ExtendedCallback<Led>.containerFrom(callback: callback!).context.toggle()
}
Conclusion
In this post, we fixed the issue we encountered last time when encapsulating our logic within a Button type.
We then encountered a limitation of closures used as C function pointers and explored several strategies on how the closure can access information defined outside of its scope.
With these core concepts covered in this post and the previous two, we have some building blocks to start tackling more complex and interesting projects.
Stay tuned as we’ll cover that in a future post.