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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
|
#+TITLE: Rust on the Flipper Zero
#+DATE: <2022-07-05 Tue 07:16>
#+TAGS: writeup rust embedded hardware flipperzero
My [[https://flipperzero.one/][Flipper Zero]] arrived in the mail a few weeks ago, ending a nearly two-year
wait for its arrival. For the uninitiated, it's a "multi-tool device for geeks":
a development board for radio, IR, and GPIO in a [[https://en.wikipedia.org/wiki/Tamagotchi][Tamagotchi]]-like form-factor. It
combines the capability of the [[https://kk4vcz.com/goodwatch/][GoodWatch]] with the cuteness of the [[https://pwnagotchi.ai/][Pwnagotchi]].
Part of the appeal, to me, is the ability to hack on the [[https://github.com/flipperdevices/flipperzero-firmware][free (as in freedom)
firmware]]. As capable as it was out of the box, providing plenty of amusement
when my brother and I took it for a spin through some parking garages, there are
still features I'd like to add to it. The problem is that I've been too pampered
by Rust as of late to want to do my firmware hacks in C.
Indeed, shortly after receiving the Flipper, I updated the firmware as directed.
While mostly solid, I had a lock-up within the first hour of use, forcing me to
give it the two-finger salute to reboot it. Obviously, these things inevitably
happen. It's early in the product's lifecycle. And even though I trust the
Flipper developers to wield the n-edged blade of C, I know that /I'm/ not a
firmware engineer by trade, and that I would want the comfort of the borrow
checker to prevent the many more locks and freezes I would introduce if I were
to write my changes in C.
Embedded software is a field where Rust (supposedly) [[https://artemis.sh/2022/03/28/oxide-hubris-on-pinetime.html][shines]],and I happened to
come across Philipp Oppermann's book /Writing an OS in Rust/ recently
(specifically [[https://os.phil-opp.com/freestanding-rust-binary/][this section]]) which inspired me to give it a whirl on my new toy.
* Shoehorning Rust Into a C Build System
#+BEGIN_EXPORT html
<header class="article-future-interjection">
#+END_EXPORT
A few days ago, the Flipper team [[https://github.com/flipperdevices/flipperzero-firmware/pull/1269][integrated scons as their new build system]], so
the information below is already out of date. We'll mention the differences in
the next post.
#+BEGIN_EXPORT html
</header>
#+END_EXPORT
Our first goal will be to get "hello world" running on the Flipper. We'll do
something actually useful with the Flipper API eventually, but we must walk
before we can run.
The firmware build process is [[https://github.com/flipperdevices/flipperzero-firmware/blob/dev/ReadMe.md][well-documented]]. It's primarily written in C and
uses =make=. We have two feasible options for shoehorning Rust into the process.
We can:
1. Use =rustc= to emit an [[https://stackoverflow.com/questions/37891559/rusts-o-file-format-not-recognized-by-gcc][object file]], which we can add to the list of objects
being linked by build system.
2. Create a proper =cargo= project and produce a [[https://en.wikipedia.org/wiki/Static_library#Creating_static_libraries_in_C/C++][static C library]], which we'll
then link into the firmware.
The latter has the benefit that we can easily use =no-std= libraries and inject
custom logic into a =build.rs= (e.g. for generating bindings as part of the build
process). But before settling on one particular solution, I wanted data on cost
in terms of firmware size -- one of several limitations we need to consider as
we hack on the firmware.
** Linking an Object File
I cracked open a beer Saturday afternoon and sat down to get through the web of
Makefiles, eventually converging on this diff to include a =rustc= rule:
#+BEGIN_SRC diff
diff --git a/applications/applications.c b/applications/applications.c
index 247c601c..67e70e8d 100644
--- a/applications/applications.c
+++ b/applications/applications.c
@@ -49,6 +49,7 @@ extern int32_t file_browser_app(void* p);
// Plugins
extern int32_t music_player_app(void* p);
extern int32_t wav_player_app(void* p);
+extern int32_t hello_world_app(void *p);
extern int32_t clock_app(void *p);
extern int32_t unirfremix_app(void *p);
extern int32_t spectrum_analyzer_app(void* p);
@@ -382,6 +383,14 @@ const FlipperApplication FLIPPER_PLUGINS[] = {
.flags = FlipperApplicationFlagDefault},
#endif
+#ifdef APP_HELLO_WORLD
+ {.app = hello_world_app,
+ .name = "Hello World",
+ .stack_size = 1024,
+ .icon = NULL,
+ .flags = FlipperApplicationFlagDefault},
+#endif
+
};
const size_t FLIPPER_PLUGINS_COUNT = COUNT_OF(FLIPPER_PLUGINS);
diff --git a/applications/applications.mk b/applications/applications.mk
index 60a339bd..baeaf4ae 100644
--- a/applications/applications.mk
+++ b/applications/applications.mk
@@ -4,6 +4,7 @@ LIB_DIR = $(PROJECT_ROOT)/lib
CFLAGS += -I$(APP_DIR)
C_SOURCES += $(shell find $(APP_DIR) -name "*.c")
CPP_SOURCES += $(shell find $(APP_DIR) -name "*.cpp")
+RUST_SOURCES += $(shell find $(APP_DIR) -name "*.rs")
RAM_EXEC ?= 0
ifeq ($(RAM_EXEC), 1)
@@ -53,6 +54,7 @@ APP_SPECTRUM_ANALYZER = 1
APP_MUSIC_PLAYER = 1
APP_SNAKE_GAME = 1
APP_WAV_PLAYER = 1
+APP_HELLO_WORLD = 1
APP_TETRIS_GAME = 1
# Debug
@@ -253,6 +255,12 @@ CFLAGS += -DAPP_WAV_PLAYER
SRV_GUI = 1
endif
+APP_HELLO_WORLD ?= 0
+ifeq ($(APP_HELLO_WORLD), 1)
+CFLAGS += -DAPP_HELLO_WORLD
+SRV_GUI = 1
+endif
+
APP_TETRIS_GAME ?= 0
ifeq ($(APP_TETRIS_GAME), 1)
CFLAGS += -DAPP_TETRIS_GAME
diff --git a/applications/hello_world/hello_world.rs b/applications/hello_world/hello_world.rs
new file mode 100644
index 00000000..93a0b1b8
--- /dev/null
+++ b/applications/hello_world/hello_world.rs
@@ -0,0 +1,15 @@
+#![no_std]
+#![crate_type = "staticlib"]
+
+use core::panic::PanicInfo;
+
+#[no_mangle]
+pub extern "C" fn hello_world_app() -> i32 {
+ 0
+}
+
+/// This function is called on panic.
+#[panic_handler]
+fn panic(_info: &PanicInfo) -> ! {
+ loop {}
+}
diff --git a/make/base.mk b/make/base.mk
index 281419e9..ec277016 100644
--- a/make/base.mk
+++ b/make/base.mk
@@ -1,7 +1,8 @@
OBJ_DIR = .obj
ASM_SOURCES =
C_SOURCES =
-CPP_SOURCES =
+CPP_SOURCES =
+RUST_SOURCES =
ASSETS =
OPENOCD_OPTS =
diff --git a/make/rules.mk b/make/rules.mk
index bdf8a4af..68b62c0c 100644
--- a/make/rules.mk
+++ b/make/rules.mk
@@ -4,11 +4,13 @@ OBJ_DIR := $(OBJ_DIR)/$(TARGET)-$(PROJECT)
C_SOURCES := $(abspath ${C_SOURCES})
ASM_SOURCES := $(abspath ${ASM_SOURCES})
CPP_SOURCES := $(abspath ${CPP_SOURCES})
+RUST_SOURCES := $(abspath ${RUST_SOURCES})
# Gather object
OBJECTS = $(addprefix $(OBJ_DIR)/, $(C_SOURCES:.c=.o))
OBJECTS += $(addprefix $(OBJ_DIR)/, $(ASM_SOURCES:.s=.o))
OBJECTS += $(addprefix $(OBJ_DIR)/, $(CPP_SOURCES:.cpp=.o))
+OBJECTS += $(addprefix $(OBJ_DIR)/, $(RUST_SOURCES:.rs=.o))
OBJECT_DIRS = $(sort $(dir $(OBJECTS)))
@@ -78,6 +80,10 @@ $(OBJ_DIR)/%.o: %.cpp $(OBJ_DIR)/BUILD_FLAGS
@echo "\tCPP\t" $(subst $(PROJECT_ROOT)/, , $<)
@$(CPP) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
+$(OBJ_DIR)/%.o: %.rs $(OBJ_DIR)/BUILD_FLAGS
+ @echo "\tRUSTC\t" $(subst $(PROJECT_ROOT)/, , $<)
+ @$(RUSTC) $< -o $@
+
$(OBJ_DIR)/flash: $(OBJ_DIR)/$(PROJECT).bin
openocd $(OPENOCD_OPTS) -c "program $(OBJ_DIR)/$(PROJECT).bin reset exit $(FLASH_ADDRESS)"
touch $@
diff --git a/make/toolchain.mk b/make/toolchain.mk
index f161d3d1..9a27ca8d 100644
--- a/make/toolchain.mk
+++ b/make/toolchain.mk
@@ -12,6 +12,7 @@ CCACHE := $(shell which ccache)
CC = $(CCACHE) $(PREFIX)gcc -std=gnu17
CPP = $(CCACHE) $(PREFIX)g++ -std=gnu++17
+RUSTC = rustc --target thumbv7em-none-eabihf --emit obj
LD = $(PREFIX)g++
AS = $(PREFIX)gcc -x assembler-with-cpp
CP = $(PREFIX)objcopy
#+END_SRC
I took my usual approach of copy/pasting the surrounding code and letting
whatever errors the build system spits out guide my changes. The mortal sin
present above is a hard-coded processor architecture. Also, note that this is
=thumbv7em-none-eabihf= and not =thumbv7em-none-eabi=, which I used in the beginning
to little success. The Flipper Zero has a STM32WB55RG MCU. Rust's "Embedded Rust
Book"[fn:4] uses a similar STM32F3DISCOVERY, but the Rust targets are different.
Using the latter target will net you an error:
#+BEGIN_SRC prog
\tRUSTC\t applications/hello_world/hello_world.rs
\tLD\t .obj/f7-firmware/firmware.elf
/opt/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld: error: .obj/f7-firmware/firmware.elf uses VFP register arguments, .obj/f7-firmware//home/jakob/Code/flipperzero-firmware/applications/hello_world/hello_world.o does not
/opt/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld: failed to merge target specific data of file .obj/f7-firmware//home/jakob/Code/flipperzero-firmware/applications/hello_world/hello_world.o
#+END_SRC
Anyway, here's the =hello_world.rs= with better syntax highlighting than what
=diff-mode= gives us:
#+BEGIN_SRC rust
#![no_std]
#![crate_type = "staticlib"]
use core::panic::PanicInfo;
#[no_mangle]
pub extern "C" fn hello_world_app() -> i32 {
0
}
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#+END_SRC
This should read as Rust, more-or-less. We specify =#![no_std]=, because we won't
have =std= available in an embedded environment. We also specify =#![crate_type =
"staticlib"]= because we're targeting a C object file and don't want an error
about an absent =main= function. We're exporting a function that's going to be
called by some C code outside =rustc='s purview, so we specify =#[no_mangle]= and
=extern "C"= -- that much should be reasonably familiar to anyone who has used
Rust's FFI interface. Finally, in =no_std= land, we don't get to assume that
there's a nice =panic= handler there for us. We have to implement that ourselves.
We don't have a great way of handling it at this point, so we'll just loop
indefinitely.[fn:5]
But before we compile with those changes, we'll need a baseline size. Building
=9d46f62be1ffa17bc8d13b68b8708ccfec17e82e= [fn:1] on Gentoo with
=gcc-arm-none-eabi-10.3-2021.10=:
#+BEGIN_SRC prog
...
\tLD\t .obj/f7-firmware/firmware.elf
text data bss dec hex filename
883808 1696 362723 1248227 130be3 .obj/f7-firmware/firmware.elf
jakob@Epsilon ~/Code/flipperzero-firmware $ du -b dist/f7/flipper-z-f7-firmware-local-9d46f62b.elf
16361148 dist/f7/flipper-z-f7-firmware-local-9d46f62b.elf
jakob@Epsilon ~/Code/flipperzero-firmware $ du -b dist/f7/flipper-z-f7-full-local-9d46f62b.dfu
885821 dist/f7/flipper-z-f7-full-local-9d46f62b.dfu
#+END_SRC
Note that =dist/= contains a couple of files -- =firmware.elf= is the actual linker
output, but =.dfu= is the output in the format that's flashed to the Flipper. Now,
we can confirm that our changes to the build system work as desired if we run it
without adding =applications/hello_world=:
#+BEGIN_SRC prog
...
\tLD\t .obj/f7-firmware/firmware.elf
/opt/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld: .obj/f7-firmware//home/jakob/Code/flipperzero-firmware/applications/applications.o:(.rodata.FLIPPER_PLUGINS+0x3c): undefined reference to `hello_world_app'
collect2: error: ld returned 1 exit status
make[1]: *** [/home/jakob/Code/flipperzero-firmware/make/rules.mk:48: .obj/f7-firmware/firmware.elf] Error 1
make[1]: Leaving directory '/home/jakob/Code/flipperzero-firmware/firmware'
make: *** [Makefile:68: firmware_all] Error 2
#+END_SRC
If we then add the file and compile,
#+BEGIN_SRC prog
...
\tLD\t .obj/f7-firmware/firmware.elf
text data bss dec hex filename
883840 1696 362691 1248227 130be3 .obj/f7-firmware/firmware.elf
jakob@Epsilon ~/Code/flipperzero-firmware $ du -b dist/f7/flipper-z-f7-firmware-local-9d46f62b.elf
16361308 dist/f7/flipper-z-f7-firmware-local-9d46f62b.elf
jakob@Epsilon ~/Code/flipperzero-firmware $ du -b dist/f7/flipper-z-f7-full-local-9d46f62b.dfu
885853 dist/f7/flipper-z-f7-full-local-9d46f62b.dfu
#+END_SRC
we can see that the impact on size (of the =.dfu=) is negligible. The overhead is
less than than 0.01%.
** Injecting a Static Library
For this evaluation, I didn't bother with integrating everything nicely with the
build system. I built a =staticlib= with =cargo build --release --target
thumbv7em-none-eabihf= and hard-coded it into the object list that's passed to
the linker. Hence, no diff is included here. We'll also need to augment the
=LDFLAGS= to allow for redefinitions or else we get cacophony.
#+BEGIN_SRC prog
\tLD\t .obj/f7-firmware/firmware.elf
/opt/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld: /opt/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/thumb/v7e-m+fp/hard/libgcc.a(_arm_cmpdf2.o): in function `__aeabi_dcmpeq':
(.text+0xac): multiple definition of `__aeabi_dcmpeq'; ../applications/hello_world/target/thumbv7em-none-eabihf/release/libhello_world.a(compiler_builtins-50ab11bfb8346963.compiler_builtins.a2c45ff5-cgu.108.rcgu.o):/cargo/registry/src/github.com-1ecc6299db9ec823/compiler_builtins-0.1.71/src/macros.rs:335: first defined here
...
/opt/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/bin/ld: /opt/gcc-arm-none-eabi-10.3-2021.10/bin/../lib/gcc/arm-none-eabi/10.3.1/thumb/v7e-m+fp/hard/libgcc.a(_arm_addsubdf3.o): in function `__floatundidf':
(.text+0x30c): multiple definition of `__aeabi_ul2d'; ../applications/hello_world/target/thumbv7em-none-eabihf/release/libhello_world.a(compiler_builtins-50ab11bfb8346963.compiler_builtins.a2c45ff5-cgu.143.rcgu.o):/cargo/registry/src/github.com-1ecc6299db9ec823/compiler_builtins-0.1.71/src/macros.rs:241: first defined here
#+END_SRC
Once all is squared away, we have a firmware image we can measure.
#+BEGIN_SRC prog
\tLD\t .obj/f7-firmware/firmware.elf
text data bss dec hex filename
887640 1696 358891 1248227 130be3 .obj/f7-firmware/firmware.elf
akob@Epsilon ~/Code/flipperzero-firmware $ du -b dist/f7/flipper-z-f7-firmware-local-9d46f62b.elf
16447736 dist/f7/flipper-z-f7-firmware-local-9d46f62b.elf
jakob@Epsilon ~/Code/flipperzero-firmware $ du -b dist/f7/flipper-z-f7-full-local-9d46f62b.dfu
889653 dist/f7/flipper-z-f7-full-local-9d46f62b.dfu
#+END_SRC
The size impact (again, for the =.dfu=), is not as negligible. The overhead is
about 0.4%.
Why is there a difference? The real answer follows later in the article -- qI
won't spoil it now. For, I'll leave it at "well, we can inspect the =.a= to see
that it's including seemingly redundant/irrelevant stuff."
#+BEGIN_SRC prog
jakob@Epsilon ~/Code/flipperzero-firmware $ ar t applications/hello_world/target/thumbv7em-none-eabihf/release/libhello_world.a
hello_world-260792d958918d7e.hello_world.d3378d15-cgu.0.rcgu.o
compiler_builtins-50ab11bfb8346963.compiler_builtins.a2c45ff5-cgu.0.rcgu.o
...
absvdi2.o
absvsi2.o
absvti2.o
addvdi3.o
addvsi3.o
addvti3.o
aeabi_cdcmpeq_check_nan.o
aeabi_cfcmpeq_check_nan.o
aeabi_div0.o
aeabi_drsub.o
aeabi_frsub.o
bswapdi2.o
bswapsi2.o
clzdi2.o
...
udivmodsi4.o
udivsi3.o
umodsi3.o
apple_versioning.o
rustc_std_workspace_core-9ae03c706dab5b95.rustc_std_workspace_core.86fb1412-cgu.0.rcgu.o
core-4c81109e506dada5.core.1ad88dac-cgu.0.rcgu.o
#+END_SRC
This is quite a bit. I had to omit about a hundred other =compiler_builtins=
objects. I came across this [[https://internals.rust-lang.org/t/rust-staticlibs-and-optimizing-for-size/5746][Rust Internals post]] from five years about using LTO
to minimize =staticlib= size. Let's give it a try... first, adding =-flto= to
=LDFLAGS=...
#+BEGIN_SRC prog
\tLD\t .obj/f7-firmware/firmware.elf
text data bss dec hex filename
887640 1696 358891 1248227 130be3 .obj/f7-firmware/firmware.elf
jakob@Epsilon ~/Code/flipperzero-firmware $ du -b dist/f7/flipper-z-f7-firmware-local-9d46f62b.elf
16447736 dist/f7/flipper-z-f7-firmware-local-9d46f62b.elf
jakob@Epsilon ~/Code/flipperzero-firmware $ du -b dist/f7/flipper-z-f7-full-local-9d46f62b.dfu
889653 dist/f7/flipper-z-f7-full-local-9d46f62b.dfu
#+END_SRC
No difference. Let's try with Rust:
#+BEGIN_SRC toml
[profile.release]
lto = true
#+END_SRC
#+BEGIN_SRC prog
\tLD\t .obj/f7-firmware/firmware.elf
text data bss dec hex filename
887640 1696 358891 1248227 130be3 .obj/f7-firmware/firmware.elf
jakob@Epsilon ~/Code/flipperzero-firmware $ du -b dist/f7/flipper-z-f7-firmware-local-9d46f62b.elf
16447736 dist/f7/flipper-z-f7-firmware-local-9d46f62b.elf
jakob@Epsilon ~/Code/flipperzero-firmware $ du -b dist/f7/flipper-z-f7-full-local-9d46f62b.dfu
889653 dist/f7/flipper-z-f7-full-local-9d46f62b.dfu
#+END_SRC
No difference. So we'll press with the first approach of linking in an object
file. This means we won't be able to use (at least not conveniently) a =build.rs=
to automate making bindings, but we can still do that from the command-line.
* Binding to the Flipper API
We'll use [[https://rust-lang.github.io/rust-bindgen/][rust-bindgen]], which allows us to parse the C/C++ header files in the
source tree and generate Rust FFI bindings automatically. We can get it by
running =cargo install bindgen=. My approach was to =cat= together all the headers
we care about run =bindgen= on that.
#+BEGIN_SRC sh
HEADERS="
../core/furi.h
../firmware/targets/furi_hal_include/furi_hal_version.h
../applications/dialogs/dialogs.h
../applications/gui/gui.h
../applications/gui/view_dispatcher.h
../applications/gui/modules/empty_screen.h
../lib/mlib/m-string.h"
cat $HEADERS > ../applications/hello_world/master.h
bindgen ../applications/hello_world/master.h --ctypes-prefix ::core::ffi --use-core -- \
$CFLAGS \
> "../applications/hello_world/master.rs"
#+END_SRC
Where =CFLAGS= is something I got =make= to output using a few conveniently-placed
=echo='s -- I had to trim it to just the =-D= declarations and =-I= include paths
because =bindgen= doesn't seem to like any other Clang arguments. And because we
can't use a crate to deal with the FFI types, I'm leveraging the nightly-only
=core_ffi_c= API. Our code changes somewhat:
#+BEGIN_SRC rust
#![no_std]
#![feature(core_ffi_c)]
#![crate_type = "staticlib"]
use core::panic::PanicInfo;
include!("master.rs");
#[no_mangle]
pub extern "C" fn hello_world_app() -> i32 {
0
}
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#+END_SRC
.. surprisingly (at least, to me) the binary size doesn't change when I
re-compile with the =include!= statement. I suppose that means that either dead
code elimination is working as intended, or the code emitted by =bindgen= is just
declarations and absolutely nothing else.
It does take a while for the compiler to stop spitting things out to my
terminal, however.
#+BEGIN_SRC prog
...
warning: constant `ViewDispatcherType_ViewDispatcherTypeFullscreen` should have an upper case name
--> master.rs:41909:11
|
41909 | pub const ViewDispatcherType_ViewDispatcherTypeFullscreen: ViewDispatcherType = 2;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: convert the identifier to upper case: `VIEW_DISPATCHER_TYPE_VIEW_DISPATCHER_TYPE_FULLSCREEN`
warning: 8304 warnings emitted
#+END_SRC
Jesus.
* Writing Hello World
I chose to rewrite (most of) =applications/about/about.c=, as it seemed most
amenable to being adapted into a "hello world" program. This might have been a
good opportunity to use [[https://c2rust.com/][C2Rust]], but I didn't think it was worth learning the
tool just to save me 200 lines of code.
#+BEGIN_SRC rust
#![no_std]
#![feature(core_ffi_c)]
#![crate_type = "staticlib"]
use core::panic::PanicInfo;
// Include the Rust code emitted by bindgen.
include!("master.inc");
/// Populate `dest` with the C string encoding of `src`.
unsafe fn write_cstr(dest: &mut [i8], src: &str) {
let mut i = 0;
for byte in src.bytes() {
// Had to use unchecked because we don't have
// `core::panicking::panic_bounds_check`. Hence, this whole function is
// `unsafe`.
,*dest.get_unchecked_mut(i) = byte as i8;
// dest[i] = byte as i8;
i += 1;
}
dest[i] = '\0' as i8;
}
#[no_mangle]
pub unsafe extern "C" fn hello_world_app(_p: *const core::ffi::c_void) -> i32 {
// Stack-allocated buffers for our converted C strings.
let mut buffer: [i8; 64] = [0; 64];
let mut buffer2: [i8; 64] = [0; 64];
write_cstr(&mut buffer, "dialogs");
let dialogs = core::mem::transmute::<*mut core::ffi::c_void, *mut DialogsApp>(
furi_record_open(buffer[..].as_ptr()),
);
let message: *mut DialogMessage = dialog_message_alloc();
write_cstr(&mut buffer, "gui");
let gui = core::mem::transmute::<*mut core::ffi::c_void, *mut Gui>(furi_record_open(
buffer[..].as_ptr(),
));
let view_dispatcher: *mut ViewDispatcher = view_dispatcher_alloc();
let empty_screen: *mut EmptyScreen = empty_screen_alloc();
let empty_screen_index: u32 = 0;
// draw empty screen to prevent menu flickering
view_dispatcher_add_view(
view_dispatcher,
empty_screen_index,
empty_screen_get_view(empty_screen),
);
view_dispatcher_attach_to_gui(
view_dispatcher,
gui,
ViewDispatcherType_ViewDispatcherTypeFullscreen,
);
view_dispatcher_switch_to_view(view_dispatcher, empty_screen_index);
let mut screen_index = 0;
let about_screens = [unleashed_info_screen, unleashed_info_screen2];
let about_screens_count = 2;
loop {
if screen_index >= about_screens_count - 1 {
write_cstr(&mut buffer, "Back");
dialog_message_set_buttons(
message,
buffer[..].as_ptr(),
core::ptr::null_mut(),
core::ptr::null_mut(),
);
} else {
write_cstr(&mut buffer, "Back");
write_cstr(&mut buffer2, "Next");
dialog_message_set_buttons(
message,
buffer[..].as_ptr(),
core::ptr::null_mut(),
buffer2[..].as_ptr(),
);
}
// let screen_result = about_screens[screen_index](dialogs, message);
let screen_result = about_screens.get_unchecked(screen_index)(dialogs, message);
if screen_result == DialogMessageButton_DialogMessageButtonLeft {
if screen_index <= 0 {
break;
} else {
screen_index -= 1;
}
} else if screen_result == DialogMessageButton_DialogMessageButtonRight {
if screen_index < about_screens_count {
screen_index += 1;
}
} else if screen_result == DialogMessageButton_DialogMessageButtonBack {
break;
}
}
dialog_message_free(message);
write_cstr(&mut buffer, "dialogs");
furi_record_close(buffer[..].as_ptr());
view_dispatcher_remove_view(view_dispatcher, empty_screen_index);
view_dispatcher_free(view_dispatcher);
empty_screen_free(empty_screen);
write_cstr(&mut buffer, "dialogs");
furi_record_close(buffer[..].as_ptr());
0
}
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
unsafe fn unleashed_info_screen(
dialogs: *mut DialogsApp,
message: *mut DialogMessage,
) -> DialogMessageButton {
let mut result: DialogMessageButton;
let screen_header = "Hello from Rust\n";
let mut buffer: [i8; 64] = [0; 64];
write_cstr(&mut buffer, screen_header);
dialog_message_set_header(
message,
buffer[..].as_ptr(),
0,
0,
Align_AlignLeft,
Align_AlignTop,
);
write_cstr(&mut buffer, screen_header);
dialog_message_set_text(
message,
buffer[..].as_ptr(),
0,
26,
Align_AlignLeft,
Align_AlignTop,
);
result = dialog_message_show(dialogs, message);
dialog_message_set_header(
message,
core::ptr::null_mut(),
0,
0,
Align_AlignLeft,
Align_AlignTop,
);
dialog_message_set_text(
message,
core::ptr::null_mut(),
0,
0,
Align_AlignLeft,
Align_AlignTop,
);
result
}
unsafe fn unleashed_info_screen2(
dialogs: *mut DialogsApp,
message: *mut DialogMessage,
) -> DialogMessageButton {
let mut result: DialogMessageButton;
let screen_header = "Hello from Rust 2\n";
let mut buffer: [i8; 64] = [0; 64];
write_cstr(&mut buffer, screen_header);
dialog_message_set_header(
message,
buffer[..].as_ptr(),
0,
0,
Align_AlignLeft,
Align_AlignTop,
);
write_cstr(&mut buffer, screen_header);
dialog_message_set_text(
message,
buffer[..].as_ptr(),
0,
26,
Align_AlignLeft,
Align_AlignTop,
);
result = dialog_message_show(dialogs, message);
dialog_message_set_header(
message,
core::ptr::null_mut(),
0,
0,
Align_AlignLeft,
Align_AlignTop,
);
dialog_message_set_text(
message,
core::ptr::null_mut(),
0,
0,
Align_AlignLeft,
Align_AlignTop,
);
result
}
#+END_SRC
This is perhaps the worst Rust code I've ever written, but it's at least a
starting point.
You may notice my comment about having "to use =unchecked_get= because we don't
have =core::panicking::panic_bounds_check=." Indeed, when I naïvely decided to use
=rustc= on its own, I was [[https://github.com/rust-lang/compiler-builtins/issues/245][actually eschewing]] some important parts of =libcore=.
Right now, we're more "on our own" than we usually would be writing =no_std= code.
The rest of the snippet is wrought with unsafe transmutation of =void *= values
that are being passed across the FFI boundary, and some gross manual conversion
(=write_cstr=) between Rust and C strings because I'm missing out on the niceness
of =std='s =CString= & friends. But it works.
... well, not without some tinkering. When I first tried to test this, I ran
into an issue that made me think my USB-C cable was bad: qFlipper was failing to
flash my firmware images, but the failure was happening while it was erasing the
internal memory -- before it could even get to sending my firmware image over
the wire.
#+BEGIN_SRC prog
20235 [DBG] Erasing memory: 94%
20329 [DBG] Erasing memory: 95%
20423 [DBG] Erasing memory: 96%
20517 [DBG] Erasing memory: 97%
20658 [DBG] Erasing memory: 98%
20843 [REG] Device went offline: VID_0x483:PID_0xdf11
21958 [DBG] Failed to perform control transfer: LIBUSB_ERROR_NO_DEVICE
21958 [DBG] Unable to get device status
21958 [DBG] An error has occured during erase phase
21958 [DBG] Failed to erase page
21958 [DBG] Failed to erase the memory
21958 [RCY] Firmware Download @Ellwt1n3 ERROR: Can't flash firmware: An error has occured during the operation.
21958 [DEV] Firmware install from file @Ellwt1n3 ERROR: Can't flash firmware: An error has occured during the operation.
21958 [BKD] Current operation finished with error: "Can't flash firmware: An error has occured during the operation."
26905 [APP] qFlipper exited
26924 [default] "There are still \"1\" items in the process of being created at engine destruction."
#+END_SRC
I tried several times with several different USB-C cables, but the error
occurred consistently, so I went to the [[https://forum.flipperzero.one/t/device-disconnect-when-erasing-memory/3731/2][Flipper Zero forums to ask about it]].
Astra was kind enough to let me know that my firmware was too big. I thought
this would mean having to golfing the code down to size, but really, no amount
of optimization was going to make that "hello world" any smaller. The firmware
image is big enough as it is,[fn:3] so I disabled a couple features I wasn't
using at the time to make room.
#+BEGIN_SRC diff
diff --git a/applications/applications.mk b/applications/applications.mk
index baeaf4ae..8096cc08 100644
--- a/applications/applications.mk
+++ b/applications/applications.mk
@@ -35,8 +35,8 @@ SRV_STORAGE = 1
# Apps
SRV_DESKTOP = 1
APP_ARCHIVE = 1
-APP_GPIO = 1
-APP_IBUTTON = 1
+APP_GPIO = 0
+APP_IBUTTON = 0
APP_INFRARED = 1
APP_LF_RFID = 1
APP_NFC = 1
@@ -46,16 +46,16 @@ APP_PASSPORT = 1
APP_UPDATER = 1
# Custom apps
-APP_UNIRFREMIX = 1
-APP_CLOCK = 1
-APP_SPECTRUM_ANALYZER = 1
+APP_UNIRFREMIX = 0
+APP_CLOCK = 0
+APP_SPECTRUM_ANALYZER = 0
# Plugins
-APP_MUSIC_PLAYER = 1
-APP_SNAKE_GAME = 1
-APP_WAV_PLAYER = 1
+APP_MUSIC_PLAYER = 0
+APP_SNAKE_GAME = 0
+APP_WAV_PLAYER = 0
APP_HELLO_WORLD = 1
-APP_TETRIS_GAME = 1
+APP_TETRIS_GAME = 0
# Debug
APP_ACCESSOR = 1
#+END_SRC
#+CAPTION: Black Flipper Zero reading "Hello from Rust".
[[./flipper-zero-hello-world.jpg]]
And I was finally greeted by my Flipper.
* Conclusion
We now have a successful proof-of-concept for running Rust code on the Flipper
Zero, even if it came at the cost losing Rust's distinctive safety features. We
also learned that we'll continue to be constrained by size limitations as we
continue to work on the firmware.
When we revisit this project next, we will:
1. Port these hacks over to the new =scons= build system.
2. Re-implement the parts of =libcore= necessary to leverage Rust's safety features.
3. Learn the Flipper's GUI library and develop a simple hex editor.
I'd like to develop a POCSAG beacon and a mechanism for loading code from the SD
card rather than having everything baked into internal memory, but these are
more ambitious, so I'll shelve them for a third article.
---
[fn:1] For those following along at home: I am building atop Eng1n33r's [[https://github.com/Eng1n33r/flipperzero-firmware][Unleashed]], rather than the official [[https://github.com/flipperdevices/flipperzero-firmware][flipperzero-firmware]].
[fn:3] My justification for this claim is this comment in the [[https://blog.flipperzero.one/new-firmware-update-system/][New Firmware Update System]] update: "a classic OTA Update is impossible for Flipper Zero. Flipper Zero's firmware is about 700KB in size and together with the Radio Stack the memory is completely exhausted. Since we don't have free space to work with, the classic OTA update won't work in our case."
[fn:4] Which, I'll admit, I haven't actually read yet. I'd only skimmed the first chapter to get started. I'm going into this mostly blind.
[fn:5] My understanding is that this isn't good to do in general. I could [[https://en.wikipedia.org/wiki/Halt_and_Catch_Fire_(computing)][halt]] here, but I'm purposefully delaying the inevitable use of [[https://doc.rust-lang.org/nightly/rust-by-example/unsafe/asm.html][inline assembly]].
|