├── .gitignore ├── Sources ├── FlipperApplication │ ├── Exports.swift │ ├── GUI.swift │ ├── DataTypes.swift │ ├── View.swift │ ├── Log.swift │ ├── MessageQueue.swift │ ├── Canvas.swift │ ├── Timer.swift │ ├── ApplicationManifest.swift │ └── ViewPort.swift ├── CFlipperApplication │ ├── include │ │ ├── module.modulemap │ │ └── CFlipperApplication.h │ └── patch_symbols.c └── Hello │ ├── Manifest.swift │ ├── SwiftIcon.swift │ ├── Entry.swift │ └── HelloView.swift ├── Resources ├── Video.mp4 └── Cover.svg ├── .gitmodules ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .vscode 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /Sources/FlipperApplication/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import CFlipperApplication 2 | -------------------------------------------------------------------------------- /Resources/Video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sameesunkaria/swift-flipperzero-hello/HEAD/Resources/Video.mp4 -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "flipperzero-firmware"] 2 | path = flipperzero-firmware 3 | url = https://github.com/flipperdevices/flipperzero-firmware 4 | -------------------------------------------------------------------------------- /Sources/CFlipperApplication/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module CFlipperApplication [system] { 2 | umbrella header "CFlipperApplication.h" 3 | export * 4 | } 5 | -------------------------------------------------------------------------------- /Sources/CFlipperApplication/include/CFlipperApplication.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | void furi_log_non_variadic(FuriLogLevel level, const char *tag, const char *string) { 10 | furi_log_print_format(level, tag, "%s", string); 11 | } 12 | -------------------------------------------------------------------------------- /Sources/FlipperApplication/GUI.swift: -------------------------------------------------------------------------------- 1 | import CFlipperApplication 2 | 3 | /// Opens a GUI record and performs the given operation. 4 | /// The GUI record is automatically closed after the operation returns. 5 | public func withGUI(perform operation: (borrowing GUI) -> Void) { 6 | let guiRecord: StaticString = "gui" 7 | let guiPointer = furi_record_open(guiRecord.utf8Start) 8 | operation(GUI(borrowing: OpaquePointer(guiPointer))) 9 | furi_record_close(guiRecord.utf8Start) 10 | } 11 | 12 | public struct GUI: ~Copyable { 13 | public var pointer: OpaquePointer 14 | 15 | public init(borrowing pointer: OpaquePointer) { 16 | self.pointer = pointer 17 | } 18 | 19 | public func add(_ viewPort: borrowing ViewPort, layer: GuiLayer) { 20 | gui_add_view_port(pointer, viewPort.pointer, layer) 21 | } 22 | 23 | public func remove(_ viewPort: borrowing ViewPort) { 24 | gui_remove_view_port(pointer, viewPort.pointer) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/FlipperApplication/DataTypes.swift: -------------------------------------------------------------------------------- 1 | import CFlipperApplication 2 | 3 | public struct Point: Hashable { 4 | public var x: Int32 5 | public var y: Int32 6 | 7 | public init(x: Int32, y: Int32) { 8 | self.x = x 9 | self.y = y 10 | } 11 | } 12 | 13 | public typealias Ticks = UInt32 14 | 15 | public enum Timeout: RawRepresentable { 16 | case waitForever 17 | case ticks(Ticks) 18 | 19 | public init(rawValue: Ticks) { 20 | if rawValue == FuriWaitForever.rawValue { 21 | self = .waitForever 22 | } else { 23 | self = .ticks(rawValue) 24 | } 25 | } 26 | 27 | public var rawValue: Ticks { 28 | switch self { 29 | case .waitForever: 30 | return FuriWaitForever.rawValue 31 | case .ticks(let ticks): 32 | return ticks 33 | } 34 | } 35 | } 36 | 37 | public struct FuriError: Error { 38 | public var code: FuriStatus 39 | 40 | public init(code: FuriStatus) { 41 | self.code = code 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Hello/Manifest.swift: -------------------------------------------------------------------------------- 1 | import FlipperApplication 2 | 3 | // The app name and the icon file names are both fixed size 32 byte strings. 4 | // Swift deos not have a fixed size string type, so we are manually writing 5 | // out the 32 element tuples for the name and icon. The ergonomics of this 6 | // may be improved by using a Swift macro but that is beyond the scope of this 7 | // project. 8 | 9 | @_used 10 | @_section(".fapmeta") 11 | let applicationManifest = ApplicationManifestV1( 12 | base: ManifestBase( 13 | manifestMagic: 0x52474448, 14 | manifestVersion: 1, 15 | // Version 86.0; Must match the version defined in 16 | // flipperzero-firmware/targets/f7/api_symbols.csv 17 | apiVersion: 0x00560000, 18 | hardwareTargetID: 7 19 | ), 20 | stackSize: 2048, 21 | appVersion: 1, 22 | // "A Swift App" in ASCII 23 | name: (65,32,83,119,105,102,116,32,65,112,112,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0), 24 | icon: ManifestIcon( 25 | hasIcon: false, 26 | name: (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0) 27 | ) 28 | ) 29 | -------------------------------------------------------------------------------- /Sources/FlipperApplication/View.swift: -------------------------------------------------------------------------------- 1 | import CFlipperApplication 2 | 3 | public struct ViewContext: ~Copyable { 4 | /// The `ViewPort` that the `View` is being drawn in. 5 | public var viewPort: ViewPort 6 | } 7 | 8 | public protocol View { 9 | /// Update the `Canvas`. Always called on the GUI thread. 10 | func draw(canvas: Canvas, context: borrowing ViewContext) 11 | 12 | /// Handle an input event. Always called on the GUI thread. 13 | func receivedInput(event: InputEvent, context: borrowing ViewContext) 14 | } 15 | 16 | public struct AnyView: View { 17 | private var draw: (Canvas, borrowing ViewContext) -> Void 18 | private var receivedInput: (InputEvent, borrowing ViewContext) -> Void 19 | 20 | public init(_ view: V) { 21 | draw = view.draw 22 | receivedInput = view.receivedInput 23 | } 24 | 25 | public func draw(canvas: Canvas, context: borrowing ViewContext) { 26 | draw(canvas, context) 27 | } 28 | 29 | public func receivedInput(event: InputEvent, context: borrowing ViewContext) { 30 | receivedInput(event, context) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/FlipperApplication/Log.swift: -------------------------------------------------------------------------------- 1 | import CFlipperApplication 2 | 3 | public enum Logger { 4 | public static let defaultTag: StaticString = "FlipperApplicaiton.Logger" 5 | 6 | public static func logError(tag: StaticString = Self.defaultTag, _ message: StaticString) { 7 | furi_log_non_variadic(FuriLogLevelError, tag.utf8Start, message.utf8Start) 8 | } 9 | 10 | public static func logWarn(tag: StaticString = Self.defaultTag, _ message: StaticString) { 11 | furi_log_non_variadic(FuriLogLevelWarn, tag.utf8Start, message.utf8Start) 12 | } 13 | 14 | public static func logInfo(tag: StaticString = Self.defaultTag, _ message: StaticString) { 15 | furi_log_non_variadic(FuriLogLevelInfo, tag.utf8Start, message.utf8Start) 16 | } 17 | 18 | public static func logDebug(tag: StaticString = Self.defaultTag, _ message: StaticString) { 19 | furi_log_non_variadic(FuriLogLevelDebug, tag.utf8Start, message.utf8Start) 20 | } 21 | 22 | public static func logTrace(tag: StaticString = Self.defaultTag, _ message: StaticString) { 23 | furi_log_non_variadic(FuriLogLevelTrace, tag.utf8Start, message.utf8Start) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Samar Sunkaria 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/FlipperApplication/MessageQueue.swift: -------------------------------------------------------------------------------- 1 | import CFlipperApplication 2 | 3 | public struct MessageQueue: ~Copyable { 4 | public var messageQueue: OpaquePointer 5 | public var capacity: UInt32 6 | 7 | public init(capacity: UInt32) { 8 | self.capacity = capacity 9 | messageQueue = furi_message_queue_alloc(capacity, UInt32(MemoryLayout.size)).unsafelyUnwrapped 10 | } 11 | 12 | deinit { 13 | furi_message_queue_free(messageQueue) 14 | } 15 | 16 | public func get(timeout: Timeout = .waitForever) throws(FuriError) -> T { 17 | let messagePointer = UnsafeMutablePointer.allocate(capacity: 1) 18 | defer { messagePointer.deallocate() } 19 | let status = furi_message_queue_get(messageQueue, messagePointer, timeout.rawValue) 20 | guard status == FuriStatusOk else { throw FuriError(code: status) } 21 | return messagePointer.pointee 22 | } 23 | 24 | public func put(_ event: T, timeout: Timeout = .waitForever) throws(FuriError) { 25 | let messagePointer = UnsafeMutablePointer.allocate(capacity: 1) 26 | defer { messagePointer.deallocate() } 27 | messagePointer.pointee = event 28 | let status = furi_message_queue_put(messageQueue, messagePointer, timeout.rawValue) 29 | guard status == FuriStatusOk else { throw FuriError(code: status) } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Hello/SwiftIcon.swift: -------------------------------------------------------------------------------- 1 | // The array was generated using fbt (Flipper Build Tool) into a *_icons.h file 2 | // and then manually copied over to this swift file. Ideally we would have some 3 | // sort of build script that would generate this swift file for us but that is 4 | // beyond the scope of this project. 5 | 6 | let swiftIcon: [UInt8] = [0x01,0x00,0xc4,0x00,0x00,0x78,0x01,0x24,0x00,0x54,0xe0,0x01,0x52,0x81,0x01,0x02,0x9e,0x02,0x04,0x05,0x48,0x20,0x0c,0x61,0x00,0x21,0x84,0x80,0x86,0x30,0x02,0x18,0x88,0x98,0x66,0x00,0x51,0x91,0x40,0x2c,0x16,0x00,0x28,0xd0,0x85,0x82,0x2d,0x2c,0x10,0x80,0x70,0x30,0x38,0x04,0x10,0x60,0x38,0x78,0x30,0x14,0xe1,0x50,0xd0,0x28,0x8d,0x86,0x2d,0x3a,0x02,0x9d,0xb2,0xcc,0x38,0x11,0xe0,0xf6,0x39,0x80,0x53,0xd8,0x66,0x60,0x30,0x82,0x28,0xd8,0x26,0xf0,0x78,0x20,0x14,0x78,0x12,0xc8,0x60,0x14,0xb0,0x17,0x19,0xc0,0x14,0xa0,0x1c,0x3b,0x04,0x12,0x20,0x05,0x18,0x37,0x02,0x0c,0x05,0x28,0x66,0x02,0x06,0x05,0x23,0x31,0x01,0x48,0xcc,0x40,0x52,0x33,0x10,0x14,0x8c,0xc4,0x05,0x28,0x01,0x98,0x61,0x09,0x72,0x33,0x0c,0x22,0x1c,0x2a,0x23,0x00,0xcd,0x12,0xe7,0x08,0xe4,0x04,0x86,0x0e,0x1d,0x18,0x84,0x7b,0xc0,0xc1,0x29,0x07,0x82,0x3e,0x05,0x2a,0x03,0x05,0x30,0x00,0x54,0x19,0x90,0x14,0xc5,0xa2,0x6e,0x1c,0x39,0x48,0xee,0x18,0x82,0x60,0x8c,0x06,0x19,0x90,0x04,0x8c,0x78,0xd6,0x3a,0x00,0x28,0xf8,0x2f,0xe1,0xd2,0x00,0x7c,0x00] 7 | let swiftIconSize: UInt16 = 48 8 | -------------------------------------------------------------------------------- /Sources/FlipperApplication/Canvas.swift: -------------------------------------------------------------------------------- 1 | import CFlipperApplication 2 | 3 | public struct Canvas { 4 | public var pointer: OpaquePointer 5 | 6 | public init(borrowing pointer: OpaquePointer) { 7 | self.pointer = pointer 8 | } 9 | 10 | public var width: Int { 11 | canvas_width(pointer) 12 | } 13 | 14 | public var height: Int { 15 | canvas_height(pointer) 16 | } 17 | 18 | public func clear() { 19 | canvas_clear(pointer) 20 | } 21 | 22 | public func setFont(_ font: Font) { 23 | canvas_set_font(pointer, font) 24 | } 25 | 26 | public var currentFontHeight: Int { 27 | canvas_current_font_height(pointer) 28 | } 29 | 30 | public func drawText(_ text: StaticString, at position: Point, horizonalAlignment: Align = AlignLeft, verticalAlignment: Align = AlignBottom) { 31 | canvas_draw_str_aligned(pointer, position.x, position.y, AlignLeft, AlignTop, text.utf8Start) 32 | } 33 | 34 | public func drawIcon(_ icon: [UInt8], at position: Point, width: UInt16, height: UInt16) { 35 | icon.withUnsafeBufferPointer { iconPointer in 36 | let frames = [iconPointer.baseAddress] 37 | frames.withUnsafeBufferPointer { framesPointer in 38 | var icon = Icon(width: width, height: height, frame_count: 1, frame_rate: 0, frames: framesPointer.baseAddress) 39 | canvas_draw_icon(pointer, position.x, position.y, &icon) 40 | } 41 | } 42 | } 43 | 44 | public func drawIcon(_ icon: inout Icon, at position: Point) { 45 | canvas_draw_icon(pointer, position.x, position.y, &icon) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Hello/Entry.swift: -------------------------------------------------------------------------------- 1 | import FlipperApplication 2 | 3 | /// The main entry point of the application. 4 | @_cdecl("entry") 5 | public func entry(pointer: UnsafeMutableRawPointer?) -> UInt32 { 6 | withGUI { gui in 7 | let model = HelloModel() 8 | let viewPort = ViewPort(HelloView(model: model)) 9 | gui.add(viewPort, layer: GuiLayerFullscreen) 10 | viewPort.update() 11 | 12 | let timer = Timer(type: FuriTimerTypePeriodic) { 13 | try? model.eventQueue.put(.timerTick) 14 | } 15 | timer.start(interval: furi_kernel_get_tick_frequency() / 6) 16 | 17 | eventLoop(model: model, viewPort: viewPort) 18 | 19 | timer.stop() 20 | viewPort.enabled(false) 21 | gui.remove(viewPort) 22 | } 23 | 24 | return 0 25 | } 26 | 27 | func eventLoop(model: HelloModel, viewPort: borrowing ViewPort) { 28 | var running = true 29 | while running { 30 | do { 31 | let event = try model.eventQueue.get(timeout: .ticks(100)) 32 | if case let .input(inputEvent) = event, 33 | inputEvent.type == InputTypePress || inputEvent.type == InputTypeRepeat, 34 | inputEvent.key == InputKeyBack 35 | { 36 | running = false 37 | } else { 38 | let oldState = model.state 39 | model.handle(event: event) 40 | 41 | // Update the view if the state has changed 42 | if model.state != oldState { 43 | viewPort.update() 44 | } 45 | } 46 | } catch { 47 | if error.code != FuriStatusErrorTimeout { 48 | running = false 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/FlipperApplication/Timer.swift: -------------------------------------------------------------------------------- 1 | import CFlipperApplication 2 | 3 | public struct Timer: ~Copyable { 4 | public var pointer: OpaquePointer 5 | public var callbackPointer: UnsafeMutablePointer<() -> Void> 6 | 7 | public init(type: FuriTimerType, callback: @escaping () -> Void) { 8 | let callbackPointer = UnsafeMutablePointer<() -> Void>.allocate(capacity: 1) 9 | callbackPointer.pointee = callback 10 | self.callbackPointer = callbackPointer 11 | pointer = furi_timer_alloc(timerCallback, type, callbackPointer).unsafelyUnwrapped 12 | } 13 | 14 | deinit { 15 | furi_timer_free(pointer) 16 | callbackPointer.deallocate() 17 | } 18 | 19 | typealias TimerCallback = @convention(c) (UnsafeMutableRawPointer?) -> Void 20 | private let timerCallback: TimerCallback = { contextPointer in 21 | contextPointer.unsafelyUnwrapped.withMemoryRebound(to: (() -> Void).self, capacity: 1) { callbackPointer in 22 | callbackPointer.pointee() 23 | } 24 | } 25 | 26 | public func start(interval ticks: Ticks) { 27 | furi_timer_start(pointer, ticks) 28 | } 29 | 30 | public func restart(interval ticks: Ticks) { 31 | furi_timer_restart(pointer, ticks) 32 | } 33 | 34 | public func stop() { 35 | furi_timer_stop(pointer) 36 | } 37 | 38 | public var isRunning: Bool { 39 | furi_timer_is_running(pointer) != 0 40 | } 41 | 42 | public func expiredTime() -> Ticks { 43 | furi_timer_get_expire_time(pointer) 44 | } 45 | 46 | public static func setThreadPriority(_ priority: FuriTimerThreadPriority) { 47 | furi_timer_set_thread_priority(priority) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/CFlipperApplication/patch_symbols.c: -------------------------------------------------------------------------------- 1 | // The symbols in this file are not exposed to a flipper app, so we need to provide 2 | // them ourselves. 3 | 4 | #include 5 | #include 6 | 7 | // This not a correct implementation for posix_memalign but seems to work for the 8 | // purposes of this example project. 9 | // 10 | // The returned pointer is not guaranteed to be a multiple of the requested alignment. 11 | // 12 | // To return correctly aligned memory, we could over-allocate and offset the pointer 13 | // returned by `malloc`; but offsetting the pointer would prevent us from using `free` 14 | // to free up the memory. Therefore, we can't correctly implement posix_memalign as a 15 | // client of `malloc` and `free`. For a correct implementation, the memory allocator 16 | // provided by the Flipper Zero firmware would need to be modified to support 17 | // posix_memalign. 18 | int posix_memalign(void** memptr, size_t alignment, size_t size) { 19 | void *mem = malloc(size); 20 | if (!mem) return 1; 21 | *memptr = mem; 22 | return 0; 23 | } 24 | 25 | // The following symbols are provided by libc and libc_nano but statically linking libc 26 | // causes us to reference other symbols that aren't exposed to a flipper app. So I 27 | // chose to provide these symbols here. 28 | 29 | void arc4random_buf(void *buf, size_t nbytes) { 30 | furi_hal_random_fill_buf(buf, nbytes); 31 | } 32 | 33 | void __aeabi_memclr(void *dest, size_t n) { 34 | memset(dest, 0, n); 35 | } 36 | 37 | void __aeabi_memclr4(void *dest, size_t n) { 38 | memset(dest, 0, n); 39 | } 40 | 41 | void __aeabi_memclr8(void *dest, size_t n) { 42 | memset(dest, 0, n); 43 | } 44 | -------------------------------------------------------------------------------- /Sources/FlipperApplication/ApplicationManifest.swift: -------------------------------------------------------------------------------- 1 | // Define the applicaiton manifest structure for the Flipper application. 2 | // It is carefully crafted to match the layout of the FlipperApplicationManifest 3 | // C struct defined in application_manifest.h. The manifest is finally placed in 4 | // the .fapmeta section of the elf binary. 5 | // 6 | // While this works, it is not ideal. Using the C struct directly might have been 7 | // a better solution but this serves as a proof of concept of generating a layout 8 | // compatible struct and placing it in a specific section of the binary entirely 9 | // in Swift. 10 | 11 | @frozen 12 | public struct ManifestBase { 13 | /// The magic constant for the manifest. Currently 0x52474448. 14 | public var manifestMagic: UInt32 15 | 16 | public var manifestVersion: UInt32 17 | 18 | /// Must match the version defined in flipperzero-firmware/targets/f7/api_symbols.csv. 19 | public var apiVersion: UInt32 20 | 21 | public var hardwareTargetID: UInt16 22 | 23 | public init( 24 | manifestMagic: UInt32, 25 | manifestVersion: UInt32, 26 | apiVersion: UInt32, 27 | hardwareTargetID: UInt16 28 | ) { 29 | self.manifestMagic = manifestMagic 30 | self.manifestVersion = manifestVersion 31 | self.apiVersion = apiVersion 32 | self.hardwareTargetID = hardwareTargetID 33 | } 34 | } 35 | 36 | public typealias CStr32 = ( 37 | CChar, CChar, CChar, CChar, 38 | CChar, CChar, CChar, CChar, 39 | CChar, CChar, CChar, CChar, 40 | CChar, CChar, CChar, CChar, 41 | CChar, CChar, CChar, CChar, 42 | CChar, CChar, CChar, CChar, 43 | CChar, CChar, CChar, CChar, 44 | CChar, CChar, CChar, CChar 45 | ) 46 | 47 | @frozen 48 | public struct ManifestIcon { 49 | public var hasIcon: Bool 50 | public var name: CStr32 51 | 52 | public init( 53 | hasIcon: Bool, 54 | name: CStr32 55 | ) { 56 | self.hasIcon = hasIcon 57 | self.name = name 58 | } 59 | } 60 | 61 | @frozen 62 | public struct ApplicationManifestV1 { 63 | public var base: ManifestBase 64 | public var stackSize: UInt16 65 | public var appVersion: UInt32 66 | public var name: CStr32 67 | public var icon: ManifestIcon 68 | 69 | public init( 70 | base: ManifestBase, 71 | stackSize: UInt16, 72 | appVersion: UInt32, 73 | name: CStr32, 74 | icon: ManifestIcon 75 | ) { 76 | self.base = base 77 | self.stackSize = stackSize 78 | self.appVersion = appVersion 79 | self.name = name 80 | self.icon = icon 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/FlipperApplication/ViewPort.swift: -------------------------------------------------------------------------------- 1 | import CFlipperApplication 2 | 3 | public struct ViewPort: ~Copyable { 4 | private enum Storage { 5 | case borrowed(viewPortPointer: OpaquePointer) 6 | case owned(viewPortPointer: OpaquePointer, contextPointer: UnsafeMutablePointer) 7 | } 8 | 9 | private struct Context: ~Copyable { 10 | var view: AnyView 11 | var viewPort: ViewPort 12 | } 13 | 14 | private var storage: Storage 15 | 16 | public init(borrowing viewPort: OpaquePointer) { 17 | storage = .borrowed(viewPortPointer: viewPort) 18 | } 19 | 20 | public init(_ view: V) { 21 | let viewPortPointer = view_port_alloc().unsafelyUnwrapped 22 | let contextPointer = UnsafeMutablePointer.allocate(capacity: 1) 23 | storage = .owned(viewPortPointer: viewPortPointer, contextPointer: contextPointer) 24 | 25 | contextPointer.pointee = Context(view: AnyView(view), viewPort: ViewPort(borrowing: viewPortPointer)) 26 | view_port_draw_callback_set(viewPortPointer, appDrawCallback, contextPointer) 27 | view_port_input_callback_set(viewPortPointer, appInputCallback, contextPointer) 28 | } 29 | 30 | deinit { 31 | if case let .owned(viewPortPointer, contextPointer) = storage { 32 | view_port_free(viewPortPointer) 33 | contextPointer.deallocate() 34 | } 35 | } 36 | 37 | typealias DrawCallback = @convention(c) (OpaquePointer?, UnsafeMutableRawPointer?) -> Void 38 | private let appDrawCallback: DrawCallback = { canvas, contextPointer in 39 | contextPointer.unsafelyUnwrapped.withMemoryRebound(to: Context.self, capacity: 1) { contextPointer in 40 | let viewPort = ViewPort(borrowing: contextPointer.pointee.viewPort.pointer) 41 | contextPointer.pointee.view.draw( 42 | canvas: Canvas(borrowing: canvas.unsafelyUnwrapped), 43 | context: ViewContext(viewPort: viewPort) 44 | ) 45 | } 46 | } 47 | 48 | typealias InputCallback = @convention(c) (UnsafeMutablePointer?, UnsafeMutableRawPointer?) -> Void 49 | private let appInputCallback: InputCallback = { inputEvent, contextPointer in 50 | contextPointer.unsafelyUnwrapped.withMemoryRebound(to: Context.self, capacity: 1) { contextPointer in 51 | let viewPort = ViewPort(borrowing: contextPointer.pointee.viewPort.pointer) 52 | contextPointer.pointee.view.receivedInput( 53 | event: inputEvent.unsafelyUnwrapped.pointee, 54 | context: ViewContext(viewPort: viewPort) 55 | ) 56 | } 57 | } 58 | 59 | public var pointer: OpaquePointer { 60 | switch storage { 61 | case .borrowed(let viewPortPointer): 62 | return viewPortPointer 63 | case .owned(let viewPortPointer, _): 64 | return viewPortPointer 65 | } 66 | } 67 | 68 | public func update() { 69 | view_port_update(pointer) 70 | } 71 | 72 | public func enabled(_ enabled: Bool) { 73 | view_port_enabled_set(pointer, enabled) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/Hello/HelloView.swift: -------------------------------------------------------------------------------- 1 | import FlipperApplication 2 | 3 | final class HelloModel { 4 | enum AnimationDirection: Hashable { 5 | case up 6 | case down 7 | 8 | var reversed: Self { 9 | self == .up ? .down : .up 10 | } 11 | } 12 | 13 | enum Event { 14 | case input(InputEvent) 15 | case timerTick 16 | case animationDirectionChanged(AnimationDirection) 17 | } 18 | 19 | struct State: Hashable { 20 | var textPosition = Point(x: 58, y: 8) 21 | var stepSize: Int32 = 2 22 | var textAnimationDirection: AnimationDirection = .down 23 | var showsFreeMemory: Bool = false 24 | } 25 | 26 | let eventQueue = MessageQueue(capacity: 8) 27 | var state = State() 28 | 29 | /// The single point of entry for mutating the state of the model. 30 | func handle(event: Event) { 31 | switch event { 32 | case .input(let inputEvent): 33 | handleInput(inputEvent) 34 | case .timerTick: 35 | state.textPosition.y += (state.textAnimationDirection == .down) ? 1 : -1 36 | case .animationDirectionChanged(let direction): 37 | Logger.logInfo("handle .animationDirectionChanged") 38 | state.textAnimationDirection = direction 39 | } 40 | } 41 | 42 | private func handleInput(_ inputEvent: InputEvent) { 43 | if inputEvent.type == InputTypePress || inputEvent.type == InputTypeRepeat { 44 | switch inputEvent.key { 45 | case InputKeyLeft: 46 | state.textPosition.x -= state.stepSize 47 | case InputKeyRight: 48 | state.textPosition.x += state.stepSize 49 | case InputKeyUp: 50 | state.textPosition.y -= state.stepSize 51 | case InputKeyDown: 52 | state.textPosition.y += state.stepSize 53 | var s = Set([1,2,3]) 54 | s.insert(4) 55 | case InputKeyOk: 56 | state.showsFreeMemory.toggle() 57 | default: 58 | break 59 | } 60 | } 61 | } 62 | } 63 | 64 | struct HelloView: View { 65 | var model: HelloModel 66 | 67 | func draw(canvas: Canvas, context: borrowing ViewContext) { 68 | canvas.clear() 69 | 70 | // Draw the Swift logo. 71 | canvas.drawIcon( 72 | swiftIcon, 73 | at: Point(x: 0, y: Int32((canvas.height - Int(swiftIconSize)) / 2)), 74 | width: swiftIconSize, 75 | height: swiftIconSize 76 | ) 77 | 78 | // Display the free memory when enabled. 79 | if model.state.showsFreeMemory { 80 | let freeMemory = freeMemoryString() 81 | canvas_draw_str_aligned(canvas.pointer, 0, 0, AlignLeft, AlignTop, freeMemory) 82 | freeMemory.deallocate() 83 | } 84 | 85 | // Display "Hello, Swift!". 86 | canvas.setFont(FontPrimary) 87 | canvas.drawText("Hello, Swift!", at: model.state.textPosition, verticalAlignment: AlignTop) 88 | 89 | // Change the text animation direction when it reaches the top or bottom edges of the screen. 90 | if model.state.textPosition.y > Int32(canvas.height - canvas.currentFontHeight) { 91 | try? model.eventQueue.put(.animationDirectionChanged(.up)) 92 | } else if model.state.textPosition.y <= 0 { 93 | try? model.eventQueue.put(.animationDirectionChanged(.down)) 94 | } 95 | } 96 | 97 | func receivedInput(event: InputEvent, context: borrowing ViewContext) { 98 | try? model.eventQueue.put(.input(event)) 99 | } 100 | 101 | private func freeMemoryString() -> UnsafeMutablePointer { 102 | let cString = UnsafeMutablePointer.allocate(capacity: 16) 103 | let freeMemory = memmgr_get_free_heap() 104 | itoa(CInt(freeMemory), cString, 10) 105 | return cString 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Resources/Cover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift on Flipper Zero — A Proof of Concept 2 | 3 |

4 |
5 | A sample app running on the Flipper Zero with the Swift icon on the left and the text 'Hello, Swift!' on the right 6 |
7 | Video 8 |
9 |

10 | 11 | [Flipper Zero](https://flipperzero.one) is a multi-tool for security researchers and pentesters. It is powered by the STM32 family of microcontrollers and has a small display, a few buttons, and radios for various communication protocols. The Flipper Zero firmware includes a variety of built-in applications and also supports running custom apps, typically written in C. 12 | 13 | With the recent developments in [Embedded Swift](https://github.com/apple/swift-evolution/blob/main/visions/embedded-swift.md), I was curious to explore the possibility of running Swift apps on the Flipper Zero. While there is extensive support for running C apps on the Flipper Zero, we are free to run any binary that can be compiled into a valid Flipper Application Package. There is already a project aimed at running [apps written in Rust on the Flipper Zero](https://github.com/flipperzero-rs/flipperzero), which served as an excellent resource for this project. 14 | 15 | ### Scope of the Project 16 | 17 | This project intends to demonstrate that it is possible to run Swift apps on the Flipper Zero. It is *not* a reference implementation or a library that you can use to build your own apps. The goal of this project is to inspire and encourage further exploration into using Swift for embedded systems. 18 | 19 | ## Building and Running the App 20 | 21 | ### Prerequisites 22 | - [**Swift Trunk Development Snapshot Build**](https://www.swift.org/download/#trunk-development-main): At the time of writing this, the Embedded mode for Swift is only available in the Trunk Development snapshot builds. You will need to install a trunk snapshot to be able to successfully build the app. 23 | - **macOS and Xcode**: The build script is currently only supported on macOS. In theory it should be fairly easy to adapt it for Linux, but I haven't tried it. On macOS you will need to have an appropriate version of Xcode that is compatible with the Swift toolchain you are using. 24 | - [**qFlipper**](https://flipperzero.one/update) (Optional): You may use qFlipper to copy the application to the Flipper Zero micro SD card. 25 | 26 | ### Steps 27 | 28 | #### Step 1: 29 | 30 | After cloning the repository you will need to fetch the Flipper Zero firmware submodule: 31 | 32 | ``` 33 | git submodule update --init --recursive 34 | ``` 35 | 36 | #### Step 2: 37 | 38 | Run `fbt` (Flipper Build Tool) in the `flipperzero-firmware` directory to download the Flipper Zero toolchain and build the firmware: 39 | 40 | ``` 41 | cd flipperzero-firmware 42 | ./fbt 43 | ``` 44 | 45 | #### Step 3: 46 | 47 | Flash the locally built firmware on to the Flipper Zero: 48 | 49 | ``` 50 | ./fbt flash_usb 51 | ``` 52 | 53 | > [!IMPORTANT] 54 | > You may skip this step if you don't want to install a custom built firmware on your Flipper Zero. If you decide to skip this step, make sure that you checkout the `flipperzero-firmware` submodule at the commit that matches the version of the firmware you have installed on your Flipper Zero. You may also need to update the `apiVersion` in [`Sources/Hello/Manifest.swift`](Sources/Hello/Manifest.swift) to match. 55 | 56 | #### Step 4: 57 | 58 | Build the Swift app: 59 | 60 | ``` 61 | cd .. 62 | ./build.sh 63 | ``` 64 | 65 | By default the build script will use the `swift-latest` toolchain installed in `/Library/Developer/Toolchains`. If you want to use a different toolchain, you can set the `TOOLCHAINS` environment variable to the identifier of the toolchain you want to use: 66 | 67 | ``` 68 | TOOLCHAINS="org.swift.59202403311a" ./build.sh 69 | ``` 70 | 71 | #### Step 5: 72 | 73 | Copy the generated `build/Hello.fap` file to the `/apps/Examples` directory on the SD card (you can use [`qFlipper`](https://github.com/flipperdevices/qFlipper) for this) and launch the app! 74 | 75 | ## From Code to a Running App: The Journey 76 | 77 | > [!Note] 78 | > This section is a retrospective on the steps I took to get Swift running on the Flipper Zero. Since I worked on this project, both the Flipper Zero firmware and Embedded Swift mode have changed significantly, so some of the steps may no longer be necessary or may have changed. 79 | 80 | ### Embedded Swift 81 | 82 | Swift published its [Vision for Embedded Systems](https://github.com/apple/swift-evolution/blob/main/visions/embedded-swift.md) in October 2023 and recently also published a blog post on [Getting Started with Embedded Swift on ARM and RISC-V Microcontrollers](https://www.swift.org/blog/embedded-swift-examples/), which includes a few examples of Swift running on bare-metal ARM and RISC-V microcontrollers. Since the Flipper Zero is powered by an STM32 microcontroller, which is based on the ARM Cortex-M, it should be possible to run Swift in the Embedded mode on the Flipper Zero. 83 | 84 | At the time of writing, Embedded Swift is still under development and only available in [Trunk Development snapshot builds](https://www.swift.org/download/#trunk-development-main). The Embedded Swift mode can be enabled by passing the `-enable-experimental-feature Embedded` flag to `swiftc`. 85 | 86 | ### Flipper Application Package 87 | 88 | A [Flipper Application Package (.fap)](https://developer.flipper.net/flipperzero/doxygen/apps_on_sd_card.html) is a binary file format for Flipper Zero applications. It is an [ELF binary](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format) with additional metadata. Specifically, it includes a `.fapmeta` section with information about the app, such as its name, icon name, and version. The `.fap` file is loaded and executed by the Flipper Zero firmware. 89 | 90 | ### Generating the Binary 91 | 92 | To generate a binary that can be used by the Flipper Zero, we need to tell the compiler the appropriate instruction set architecture and the environment we are targeting. We can do this by passing the desired target triple to `swiftc` using the `-target` flag. In our case, we need the `armv7-none-none-eabi` target triple, which is one of the supported triples for Embedded Swift. It generates an ARMv7 ELF object file. For the Flipper Zero we specifically need to target the [Thumb instruction set architecture](https://developer.arm.com/Architectures/T32%20Instruction%20Set%20Architecture), which we can do by additionally passing the `-mthumb` flag to `clang`. 93 | 94 | ```sh 95 | SWIFT_FLAGS+=" -target armv7-none-none-eabi -Xcc -mthumb" 96 | ``` 97 | 98 | Now we can compile a Swift source file using `swiftc`: 99 | 100 | ```sh 101 | swiftc $SWIFT_FLAGS Hello.swift -o Hello.o 102 | ``` 103 | 104 | At the time of writing, the Swift trunk development snapshot builds do not seem to include standard libraries for the `armv7-none-none-eabi` target triple. This means that `swiftc` fails at link time when compiling for that target. [This might be an issue with the contents of the Swift toolchain](https://x.com/harlanhaskins/status/1783197237897748571). For now, we can skip the linking step by passing the `-emit-object` (or `-c`) flag to `swiftc`. We can also pass the `-nostdlib` flag to the linker to prevent it from linking the standard libraries. We'll need to link the standard libraries ourselves once we start using symbols provided by them. 105 | 106 | ```sh 107 | SWIFT_FLAGS+=" -emit-object" 108 | LDFLAGS+=" -nostdlib" 109 | ``` 110 | 111 | To generate a valid Flipper Application Package (`.fap`) binary, we also need to add the `.fapmeta` section to the object file containing the application manifest metadata. The manifest is represented as a C struct in the Flipper Zero firmware source code. 112 | 113 | We should be able to directly initialize this C struct from Swift, but for this project I decided to define an equivalent struct in Swift and carefully match its layout. While that was fun to experiment with, I would suggest directly using the C struct instead, as that takes care of the memory layout for you. 114 | 115 | Swift has experimental support for placing global constants in custom ELF sections using the `@_section` attribute under the `SymbolLinkageMarkers` experimental feature. This allows us to declare the application manifest metadata instance and place it in the correct section from right within our Swift code. 116 | 117 | ```swift 118 | @_used 119 | @_section(".fapmeta") 120 | let applicationManifest = ApplicationManifestV1( 121 | ... 122 | ) 123 | ``` 124 | 125 | Note that we need the `@_used` attribute to ensure that the `applicationManifest` is not optimized away by the compiler. 126 | 127 | We also need to define an entry point to the application, which is the function that gets called when our app is launched by the Flipper Zero firmware. We can do that by declaring a function with the `@_cdecl` attribute to indicate that we want to use the C calling convention. This function is expected to take a void pointer as an argument. 128 | 129 | ```swift 130 | @_cdecl("app_entry") 131 | public func entry(pointer: UnsafeMutableRawPointer?) -> UInt32 { 132 | ... 133 | } 134 | ``` 135 | 136 | We can then pass in the C name of the function with the `--entry` (or `-e`) flag to the linker. 137 | 138 | ```sh 139 | LDFLAGS+=" -Wl,-e,app_entry" 140 | ``` 141 | 142 | You can now recompile the Swift file with the latest changes. The linker is then run as a separate step using the following command: 143 | 144 | ```sh 145 | clang $LD_FLAGS Hello.o -o Hello.fap 146 | ``` 147 | 148 | Now we have an executable that can be launched as an app on the Flipper Zero. You can copy this executable to the `/apps/Examples` directory on the SD card using `qFlipper`. 149 | 150 | However, when you launch the app, you will encounter the following error: 151 | 152 | ``` 153 | Update Firmware to use with this Application (MissingImports) 154 | ``` 155 | 156 | ### Reading the Logs 157 | 158 | We will need the logs to debug this issue further. I am using the [WiFi Devboard for Flipper Zero](https://shop.flipperzero.one/products/wifi-devboard) with the [Black Magic Probe](https://github.com/blackmagic-debug/blackmagic) firmware to read Flipper Zero logs via UART. You can find the instructions here: [Reading logs via the Dev Board](https://docs.flipper.net/development/hardware/wifi-developer-board/reading-logs). 159 | 160 | Once you have it set up with the log level on the Flipper Zero set to "Debug", you can launch the app again. You will see the following messages when the app is being loaded: 161 | 162 | ``` 163 | 13843 [I][Loader] Loading /ext/apps/Examples/Hello.fap 164 | 13960 [E][Elf] Undefined relocation 3 165 | 14129 [E][Elf] Undefined relocation 3 166 | 14148 [E][Elf] Undefined relocation 3 167 | 14153 [E][Elf] No symbol address of __stack_chk_fail 168 | 14158 [E][Elf] No symbol address of __stack_chk_guard 169 | 14160 [E][Elf] Undefined relocation 3 170 | 14163 [E][Elf] No symbol address of __stack_chk_guard 171 | 14167 [E][Elf] Undefined relocation 3 172 | 14180 [E][Elf] Undefined relocation 3 173 | 14208 [E][Elf] Undefined relocation 3 174 | 14225 [E][Elf] No symbol address of posix_memalign 175 | 14227 [E][Elf] No symbol address of posix_memalign 176 | 14230 [E][Elf] No symbol address of posix_memalign 177 | 14297 [E][Elf] Undefined relocation 3 178 | 14332 [E][Elf] Error relocating section '.text' 179 | 14391 [I][Elf] Total size of loaded sections: 10982 180 | 14394 [E][Loader] Status [3]: Load failed, /ext/apps/Examples/Hello.fap: Update Firmware to use with this Application (MissingImports) 181 | ``` 182 | 183 | The error message `No symbol address of __stack_chk_guard` indicates that the Flipper Zero firmware is unable to resolve the symbol `__stack_chk_guard`. This symbol, along with `__stack_chk_fail` is used by Swift for stack protection. We can disable stack protectors by passing the `-Xfrontend -disable-stack-protector` flag to `swiftc`. This is helpfully documented in the [Embedded Swift User Manual](https://github.com/apple/swift/blob/main/docs/EmbeddedSwift/UserManual.md). 184 | 185 | `posix_memalign` is required for dynamic memory allocations, which we can also disable for now by passing the `-no-allocations` flag to `swiftc`. 186 | 187 | ```sh 188 | SWIFT_FLAGS+=" -Xfrontend -disable-stack-protector -no-allocations" 189 | ``` 190 | 191 | After building and installing the app with the updated flags, we can launch it again. This time we see that only one kind of error is remaining: 192 | 193 | ``` 194 | 13885 [I][Loader] Loading /ext/apps/Examples/Hello.fap 195 | 13997 [E][Elf] Undefined relocation 3 196 | 14032 [E][Elf] Undefined relocation 3 197 | 14050 [E][Elf] Undefined relocation 3 198 | 14057 [E][Elf] Undefined relocation 3 199 | 14061 [E][Elf] Undefined relocation 3 200 | 14070 [E][Elf] Undefined relocation 3 201 | 14075 [E][Elf] Undefined relocation 3 202 | 14216 [E][Elf] Undefined relocation 3 203 | 14221 [E][Elf] Undefined relocation 3 204 | 14226 [E][Elf] Undefined relocation 3 205 | 14268 [E][Elf] Undefined relocation 3 206 | 14304 [E][Elf] Error relocating section '.text' 207 | 14361 [I][Elf] Total size of loaded sections: 10826 208 | 14364 [E][Loader] Status [3]: Load failed, /ext/apps/Examples/Hello.fap: Update Firmware to use with this Application (MissingImports) 209 | ``` 210 | 211 | The error message `Undefined relocation 3` indicates that the Flipper Zero firmware is unable to resolve a relocation. Diving into the Flipper Zero firmware source code, [we can see that the error is thrown in `elf_file.c`](https://github.com/flipperdevices/flipperzero-firmware/blob/890c9e87ceac86dc3d70dd3f09657483b1c4209b/lib/flipper_application/elf/elf_file.c#L348) when loading the application. The firmware does not support resolving `R_ARM_REL32` relocations (which corresponds to the raw value of 3). 212 | 213 | ### Relocations 214 | 215 | Relocation entries are records that indicate where adjustments to symbol addresses are needed. When the executable is loaded into memory, the loader resolves these relocations by updating memory locations with the actual runtime addresses of the symbols. 216 | 217 | `R_ARM_REL32` is one such type of a relocation entry. This type of relocation entry is generated by `swiftc` in a few instances when compiling for the `armv7-none-none-eabi` target triple. While there may be ways to influence the type of relocation entries that are generated by `swiftc`, I decided to add support for the `R_ARM_REL32` relocation to the Flipper Zero firmware instead. 218 | 219 | Upon examining the [`elf_relocate_symbol` function in `elf_file.c`](https://github.com/flipperdevices/flipperzero-firmware/blob/890c9e87ceac86dc3d70dd3f09657483b1c4209b/lib/flipper_application/elf/elf_file.c#L325) from the Flipper Zero firmware source code, we find that it only supports a few types of relocations (including `R_ARM_ABS32`) but `R_ARM_REL32` is not one of them. After a quick read through the "Relocation types" section of the [ARM ELF Specification](https://developer.arm.com/documentation/espc0003/1-0/?lang=en), I saw that the `R_ARM_REL32` relocation modifies the 32-bit word at the address being relocated, just like `R_ARM_ABS32`, but the value is resolved using the following formula: 220 | 221 | ``` 222 | S - P + A 223 | ``` 224 | 225 | where `S` is the resolved address of the symbol, `P` is the address of the location being relocated, and `A` is the addend (value extracted from the storage unit being relocated, in this case). 226 | 227 | We can support this relocation type fairly easily by adding an additional case for `R_ARM_REL32` to the `elf_relocate_symbol` function, implemented as follows: 228 | 229 | ```c 230 | case R_ARM_REL32: 231 | *((uint32_t*)relAddr) += symAddr - relAddr; 232 | break; 233 | ``` 234 | 235 | *I have created a [PR with this change](https://github.com/flipperdevices/flipperzero-firmware/pull/3631) on the Flipper Zero firmware repository. Hopefully it can be integrated into the firmware, allowing us to run Swift apps on the Flipper Zero without any additional modifications. (EDIT: It has since been merged.)* 236 | 237 | After building the updated firmware and flashing it to the Flipper Zero, we can try launching the app again. This time we are greeted to a successfully running app! We haven't used any of the Flipper Zero APIs yet, so we can't really display anything, but we should be able to log messages. Let's try putting some text on the screen. 238 | 239 | ### Flipper Zero APIs 240 | 241 | The Flipper Zero firmware provides a set of APIs that can be used to conveniently interact with the hardware, like putting text on the display, reading button presses, etc. To use those APIs from Swift, we can provide the relevant C header paths to the `swiftc` invocation using the `-I` flag: 242 | 243 | ```sh 244 | SWIFT_FLAGS+="\ 245 | -I $FLIPPER_REPOROOT/applications/services \ 246 | -I $FLIPPER_REPOROOT/targets/furi_hal_include \ 247 | -I $FLIPPER_REPOROOT/targets/f18/furi_hal \ 248 | -I $FLIPPER_REPOROOT/targets/f7/furi_hal \ 249 | -I $FLIPPER_REPOROOT/targets/f7/inc \ 250 | -I $FLIPPER_REPOROOT/furi \ 251 | -I $FLIPPER_REPOROOT/lib/mlib \ 252 | -I $FLIPPER_REPOROOT/lib/cmsis_core \ 253 | -I $FLIPPER_REPOROOT/lib/stm32wb_hal/Inc \ 254 | -I $FLIPPER_REPOROOT/lib/stm32wb_cmsis/Include \ 255 | -I $FLIPPER_TOOLCHAIN/arm-none-eabi/include" 256 | ``` 257 | 258 | Then we can either use a bridging header to import the relevant C headers ([as seen in the swift-embedded-examples repository](https://github.com/apple/swift-embedded-examples/blob/main/nrfx-blink-sdk/BridgingHeader.h)), or we can define a clang module ([as seen in the swift-playdate-examples repository](https://github.com/apple/swift-playdate-examples/blob/main/Sources/CPlaydate/include/module.modulemap)). I decided to go with the latter approach. 259 | 260 | For convenience, we can create a new header file with all the relevant header imports: 261 | 262 | ```c 263 | // CFlipperApplication.h 264 | 265 | #include 266 | #include 267 | #include 268 | #include 269 | #include 270 | ``` 271 | 272 | Then we can define a module map file: 273 | 274 | ``` 275 | module CFlipperApplication [system] { 276 | umbrella header "CFlipperApplication.h" 277 | export * 278 | } 279 | ``` 280 | 281 | Note the `system` attribute in the module declaration, which tells the compiler to consider the headers as system headers and therefore suppress all warnings generated from them. 282 | 283 | Some of the imported headers also require us to define preprocessor macros for the exact type of the microcontroller used by the Flipper Zero. We can do that using the `-D` flag: 284 | 285 | ```sh 286 | SWIFT_FLAGS+="\ 287 | -Xcc -DSTM32WB55xx \ 288 | -Xcc -DDSTM32WB" 289 | ``` 290 | 291 | We also need to add the module to the import search paths: 292 | 293 | ```sh 294 | SWIFT_FLAGS+=" -I $SRCROOT/CFlipperApplication/include" 295 | ``` 296 | 297 | Now we are able to use the Flipper Zero APIs from Swift. As an example, we should be able to put text on the display using the following code: 298 | 299 | ```swift 300 | import CFlipperApplication 301 | 302 | let appDrawCallback: @convention(c) (OpaquePointer?, UnsafeMutableRawPointer?) -> Void = { canvas, _ in 303 | canvas_clear(canvas) 304 | canvas_set_font(canvas, FontPrimary) 305 | let message: StaticString = "Hello, Swift!" 306 | canvas_draw_str_aligned(canvas, 8, 8, AlignLeft, AlignTop, message.utf8Start) 307 | } 308 | 309 | @_cdecl("entry") 310 | public func entry(pointer: UnsafeMutableRawPointer?) -> UInt32 { 311 | let viewPort = view_port_alloc() 312 | view_port_draw_callback_set(viewPort, appDrawCallback, nil) 313 | 314 | let GUI_RECORD: StaticString = "gui" 315 | let gui = furi_record_open(GUI_RECORD.utf8Start) 316 | gui_add_view_port(OpaquePointer(gui), viewPort, GuiLayerFullscreen) 317 | view_port_update(viewPort); 318 | 319 | while true { 320 | // Wait forever 321 | } 322 | 323 | view_port_enabled_set(viewPort, false) 324 | gui_remove_view_port(OpaquePointer(gui), viewPort) 325 | view_port_free(viewPort) 326 | furi_record_close(GUI_RECORD.utf8Start) 327 | return 0 328 | } 329 | ``` 330 | 331 | When you launch the app, you should see "Hello, Swift!" on the display! 332 | 333 | ### ABI Compatibility 334 | 335 | At this point I had a Swift app that displayed static text on the screen. As I added more features to the app, I started encountering some unexpected behaviors and even unexplainable crashes. After some debugging, I noticed that the members of one of the struct instances had wildly different values from what I expected. This pointed towards mismatched memory layouts and I realized that there are a few additional challenges that I needed to address. 336 | 337 | Unlike the examples in the [Getting Started with Embedded Swift on ARM and RISC-V Microcontrollers](https://www.swift.org/blog/embedded-swift-examples/) blog post, which run on bare-metal, in our case the Flipper Zero firmware loads and runs our `.fap` binary. We therefore need to ensure that the binary we generate expects the same memory layout for objects vended by the system and the uses the same calling conventions. This is something that Rauhul Varma also had to deal with while [building games for the Playdate in Swift](https://www.swift.org/blog/byte-sized-swift-tiny-games-playdate/#running-on-the-hardware-again). 338 | 339 | I ended up compiling a C application for the Flipper Zero on the side, and then I was able to copy over the relevant flags that were passed to the `arm-none-eabi-gcc` invocation by the Flipper Build Tool (fbt). This included flags such as `-fshort-enums`, to ensure that we match the memory layout of enums on the Flipper Zero and `-mfloat-abi=hard` to use the hardware floating point unit. 340 | 341 | ```sh 342 | SWIFT_FLAGS+="\ 343 | -Xfrontend -experimental-platform-c-calling-convention=arm_aapcs_vfp \ 344 | -Xcc -fshort-enums \ 345 | -Xcc -mcpu=cortex-m4 \ 346 | -Xcc -mfloat-abi=hard \ 347 | -Xcc -mfpu=fpv4-sp-d16" 348 | ``` 349 | 350 | Now the Swift app was running stably on the Flipper Zero. We are quite limited in the set of Swift standard library types that we can use because we had disabled dynamic memory allocations. I wanted to see if I could get dynamic allocations working. 351 | 352 | ### Patching Missing Symbols 353 | 354 | To enable dynamic memory allocations Swift relies on the `posix_memalign` function. This is again helpfully documented in the [Embedded Swift User Manual](https://github.com/apple/swift/blob/main/docs/EmbeddedSwift/UserManual.md). The Flipper Zero firmware does not provide an implementation for it, so we need to provide our own. 355 | 356 | The Flipper Zero API does include an `aligned_malloc` function, which ensures aligned memory allocations. However, it requires you to use the `aligned_free` function to free the memory. Memory allocated using `posix_memalign` is expected to be freed using the `free` function, which means that we can't directly use `aligned_malloc` to implement `posix_memalign`. 357 | 358 | We could write our own implementation for `posix_memalign` which over-allocates and offsets the pointer, but we would run into a similar problem with `free`, because `free` expects to receive a pointer that was returned by `malloc`. While not ideal, I ended up directly calling `malloc` without constraining the alignment. This is not a correct implementation but seems to work for the purposes of this project. Ideally, the Flipper Zero firmware could be extended to provide a correct implementation for `posix_memalign`. 359 | 360 | To use dictionaries and sets in Swift, we also need to provide an implementation for `arc4random_buf`, which can be done by forwarding the call to `furi_hal_random_fill_buf`. 361 | 362 | You can find the implementation of these functions in [`Sources/CFlipperApplication/patch_symbols.c`](Sources/CFlipperApplication/patch_symbols.c). This file is compiled separately and linked to the main app's object file. 363 | 364 | ```sh 365 | $LD_EXEC ${=LD_FLAGS} \ 366 | $BUILDROOT/Hello.o \ 367 | $BUILDROOT/patch_symbols.o \ 368 | -o $BUILDROOT/Hello.fap 369 | ``` 370 | 371 | Finally we are at a point where we can use most of the Swift features! (Except Strings, which are not available in the Embedded Swift mode at the time of writing) 372 | 373 | ### The Compiler Runtime Library 374 | 375 | However, there's one last thing that we need to fix. While experimenting with the app, I started hitting the following loader error whenever I included code that tried to divide an integer by `6`. I was able to divide by `4` or `8` just fine, but not by `6`. 376 | 377 | ``` 378 | No symbol address of __aeabi_uldivmod 379 | ``` 380 | 381 | Looking into `__aeabi_uldivmod`, I found that it is a helper function used by the compiler to perform division and modulo operations on unsigned long integers. It is usually provided by the compiler's runtime library, like `libgcc` or Clang's `compiler-rt`. Since the Swift trunk development snapshot builds do not include standard libraries for the `armv7-none-none-eabi` target triple, I decided to link the `libgcc` that comes as part of the `arm-none-eabi-gcc` cross compiler included with the Flipper Zero toolchain. It also provides the `__aeabi_uldivmod` symbol that we need. This approach likely has its own set of challenges, but it seems to work for now and requires much less effort than compiling my own runtime library. 382 | 383 | ```sh 384 | LDFLAGS+=" -L$FLIPPER_TOOLCHAIN/lib/gcc/arm-none-eabi/12.3.1/thumb/v7e-m+fp/hard -lgcc" 385 | ``` 386 | 387 | After linking `libgcc` and running the app for one last time, I was able to divide by `6` without any issues. We are now successfully able to run a Swift app on the Flipper Zero with most of the Swift features available to us! 388 | 389 | ### Hello, Swift! 390 | 391 | For the example app in this project, I decided to display the Swift logo alongside the text "Hello, Swift!". The text bounces vertically on the screen. You can move the text around using the arrow buttons. You can also display the amount of available memory by pressing the center button. 392 | 393 | Icons used by a Flipper Zero app are embedded into the `.fap` binary. The Flipper Build Tool (`fbt`) can generate a C file with byte arrays containing the icon data. Normally that C file is compiled and statically linked with the main application into a single `.fap` file. I instead used this feature to generate a byte array of the Swift logo and then manually copied it over to a Swift source file. Ideally, we should be able to write a code generator to directly generate a Swift source file with the images, but that is beyond the scope of this project. 394 | 395 | To enhance the development experience I also wrote Swift wrappers for some commonly used C APIs that are provided by the Flipper Zero. This helped me write code that felt more Swift-y and less C-like. :P 396 | 397 | You can find the source code for the example app in [`Sources/Hello`](Sources/Hello). 398 | 399 | ## Final Thoughts 400 | 401 | As things stand today, there are a few constraints that heavily limit the usability and experience of programming in Swift for the Flipper Zero: 402 | 403 | - No support for Strings: This is a big limitation, as Strings are a fundamental part of Swift. I frequently ran into this when attempting to log specific pieces of data from Swift. Though, there is some hope as [support for Strings is currently WIP](https://github.com/apple/swift/blob/main/docs/EmbeddedSwift/EmbeddedSwiftStatus.md). 404 | - No debugger support: This might be an interesting challenge in itself. 405 | - The Flipper C APIs are not Swift-friendly: While you can work with them directly, it's much more cumbersome than using them from C. You are almost forced to write a wrapper to have a good experience. 406 | 407 | Despite these limitations, getting Swift to run on the Flipper Zero was a fun project. It highlights the potential of using Swift for embedded systems, especially with the ongoing development work. 408 | 409 | ## Helpful Resources 410 | 411 | - [Flipper Zero Firmware](https://github.com/flipperdevices/flipperzero-firmware) 412 | - [Embedded Swift Example Projects](https://github.com/apple/swift-embedded-examples) 413 | - [Embedded Swift User Manual](https://github.com/apple/swift/blob/main/docs/EmbeddedSwift/UserManual.md) 414 | - [Rust for Flipper Zero](https://github.com/flipperzero-rs/flipperzero) 415 | - [Byte-sized Swift: Building Tiny Games for the Playdate](https://www.swift.org/blog/byte-sized-swift-tiny-games-playdate/) 416 | - [ARM ELF Specification](https://developer.arm.com/documentation/espc0003/1-0/?lang=en) 417 | - [Swift Programming Language](https://github.com/apple/swift) 418 | - [The LLVM Compiler Infrastructure](https://github.com/llvm/llvm-project) 419 | --------------------------------------------------------------------------------