title | theme |
---|---|
Embedded Rust and Serde |
black |
Meeting Embedded
2019-11-13
::: notes
:::
- core team: rust embedded working group
- managing director at ferrous systems
- embedded systems!
- robotics
- safety critical consulting
- avionics
- gas detection
- internet of things
- used to be a lot of
c
- then a little more
python
- nowadays mostly
rust
with a littlec
- (i've never been good at
c++
)
make embedded systems talk to other systems
- sensors and actuators
- other embedded systems
- bigger embedded systems
- nearby pcs
- servers, phones, or pcs over the internet
::: notes
- We live in a digital world! Everything is just bits and bytes
- To send messages we just pick up the bytes over here, and we put them over there!
- just hit the data with a memcpy, and off to lunch we go :::
c data types are not a good message format
::: notes
- Getting started is easy, getting it right is hard
- Lots of details to get wrong
- Errors are subtle, but can have a big impact :::
typedef struct {
unsigned short int adc_id;
unsigned int adc_counts;
int millivolts;
} AdcReading;
typedef enum {
ADC_IDX_TEMPERATURE, // 0
ADC_IDX_HUMIDITY, // 1
ADC_IDX_COUNT, // 2
} AdcIdx;
_Static_assert(sizeof(AdcIdx) == sizeof(int), ":)");
// compiled with -fshort-enums
typedef enum {
ADC_IDX_TEMPERATURE, // 0
ADC_IDX_HUMIDITY, // 1
ADC_IDX_COUNT, // 2
} AdcIdx;
_Static_assert(1==sizeof(AdcIdx), ":/");
// compiled with -fshort-enums
typedef enum {
ADC_IDX_TEMPERATURE, // 0
ADC_IDX_HUMIDITY, // 1
// ....
ADC_IDX_AMPERAGE, // 255
ADC_IDX_COUNT, // 256
} AdcIdx;
_Static_assert(2==sizeof(AdcIdx), ":(");
- ISO C:
sizeof(int)
- ISO C++: "big enough"
- IAR EWARM for C/C++: "big enough"
typedef struct {
uint8_t adc_id;
uint32_t adc_counts;
int32_t millivolts;
} AdcReading;
typedef struct {
uint8_t adc_id;
uint32_t adc_counts;
int32_t millivolts;
} AdcReading;
_Static_assert(9 == sizeof(AdcReading), ":)");
typedef struct {
uint8_t adc_id;
// uint8_t padding[3];
uint32_t adc_counts;
int32_t millivolts;
} AdcReading;
_Static_assert(12 == sizeof(AdcReading), ":)");
typedef struct {
uint8_t adc_id;
uint32_t adc_counts;
int32_t millivolts;
} __attribute__((packed)) AdcReading;
_Static_assert(9 == sizeof(AdcReading), ":)");
AdcReading demo[10] = { 0 };
for(int i = 0; i <= 10; i++) {
demo[i].adc_id = i;
// unaligned access
demo[i].adc_counts = get_adc_counts(i);
demo[i].millivolts = demo.adc_counts * SCALING_FACTOR;
}
some devices (arm) don't allow unaligned access at all
uint8_t tx_buffer[sizeof(Message)];
memcpy(tx_buffer, &message, sizeof(tx_buffer));
send(tx_buffer, sizeof(tx_buffer));
uint8_t rx_buffer[sizeof(Message)];
recv(rx_buffer, sizeof(rx_buffer));
Message* msg = (Message*)rx_buffer;
void process_counts(uint32_t* counts) {
uint32_t last_count = *counts;
// ...
return;
}
uint8_t rx_buffer[sizeof(Message)];
recv(rx_buffer, sizeof(rx_buffer));
Message* msg = (Message*)rx_buffer;
// uh oh
process_counts(&msg->adc_counts);
AdcReading data = {
.adc_id = 0x12,
.adc_counts = 0xA1A2B3B4,
.millivolts = -4300,
};
// Little Endian
// [0x12][0x??][0x??][0x??]
// [0xB4][0xB3][0xA2][0xA1]
// [0x34][0xEF][0xFF][0xFF]
// Big Endian
// [0x??][0x??][0x??][0x12]
// [0xA1][0xA2][0xB3][0xB4]
// [0xFF][0xFF][0xEF][0x34]
- code size
- cpu time
- memory usage
- size on the wire
- a compiled systems language
- based on llvm
- ships a stable release every 6 weeks
- supports hosted and bare metal environments
- 2006: started
- 2009: official mozilla project
- 2015: 1.0
- 2018: second edition
- 2018: stable bare metal support
- amazon
- microsoft
- mozilla
- cloudflare
- modern tooling
- cross compilation
- non-optional static analysis
- first class documentation
- convenience without compromise
- features like iterators, generics, hygienic macros...
take the best concepts from the last 30 years
evolutionary rather than revolutionary
- memory safety
- strong types
- information drives optimization
- move correctness from runtime to compile time
- build system
- package manager
- documentation generation tool
- testing framework
- extensible, but sane defaults
("the data")
struct AdcReading {
adc_id: u8,
adc_counts: u32,
millivolts: i32,
}
("the format")
- json
- yaml
- toml
- CBOR
- Pickle (python)
- assorted binary formats
- like interfaces in C++
Serialize
(frontend)Serializer
(backend)Deserialize
(frontend)Deserializer
(backend)
- primitives (
u8
/i32
/f64
/bool
) - arrays
- structs
- maps
- enums
- 29 items in total
#[derive(Serialize, Deserialize)]
struct AdcReading {
adc_id: u8,
adc_counts: u32,
millivolts: i32,
}
let reading = AdcReading {
adc_id: 0,
adc_counts: 0x1234_4567,
millivolts: -1_000,
};
let _ = serde_json::to_string(&reading);
let _ = serde_yaml::to_string(&reading);
let reading = AdcReading {
adc_id: 0,
adc_counts: 0x1234_4567,
millivolts: -1_000,
};
let mut buffer = [0u8; 128];
let _ = ssmarshal::serialize(&mut buffer, &reading);
let _ = postcard::to_slice(&reading, &mut buffer);
struct AdcReading {
adc_id: u8,
adc_counts: u32,
millivolts: i32,
}
#[repr(C)]
struct AdcReading {
adc_id: u8,
adc_counts: u32,
millivolts: i32,
}
// from `ssmarshal`
pub fn serialize<T: Serialize>(
buf: &mut [u8],
val: &T
) -> Result<usize, Error>
{
/* ... */
}
- c doesn't have slices
- pointer + max length
- c doesn't have generics
- we can monomorphize
- c doesn't have result types
- we can use flags
#[no_mangle]
pub extern "C" fn serialize_AdcReading(
input: *const AdcReading,
output_buf: *mut u8,
output_buf_max_sz: usize,
output_buf_used: *mut usize,
) -> bool {
/* ... */
}
#[repr(C)]
struct AdcReading {
adc_id: u8,
adc_counts: u32,
millivolts: i32,
}
#[no_mangle]
pub extern "C" fn serialize_AdcReading(
input: *const AdcReading,
output_buf: *mut u8,
output_buf_max_sz: usize,
output_buf_used: *mut usize,
) -> bool { /* ... */ }
Converts Rust FFI code to C headers
#[derive(Serialize, Deserialize)]
#[repr(C)]
struct AdcReading {
adc_id: u8,
adc_counts: u32,
millivolts: i32,
}
typedef struct {
uint8_t adc_id;
uint32_t adc_counts;
uint32_t millivolts;
} AdcReading;
#[no_mangle]
pub extern "C" fn serialize_AdcReading(
input: *const AdcReading,
output_buf: *mut u8,
output_buf_max_sz: usize,
output_buf_used: *mut usize,
) -> bool { /* ... */ }
bool serialize_AdcReading_t(
const AdcReading_t *input,
uint8_t *output_buf,
uintptr_t output_buf_max_sz,
uintptr_t *output_buf_used
);
typedef struct {
uint8_t adc_id;
uint32_t adc_counts;
uint32_t millivolts;
} AdcReading;
we always serialize and deserialize the data on the wire
local representation doesn't matter
rust can make staticlibs (.a
)
or dynamic libraries (.so
or similar)
or both!
[package]
name = "serializer"
[dependencies.serde]
version = "1.0"
default-features = false
features = ["derive"]
[dependencies.ssmarshal]
version = "1.0"
default-features = false
[package]
name = "serializer"
[lib]
crate-type = ["staticlib", "dylib"]
[dependencies.serde]
version = "1.0"
default-features = false
features = ["derive"]
[dependencies.ssmarshal]
version = "1.0"
default-features = false
- run
cargo build --release
- run
cbindgen
- include the generated header file
- link in the library
turn C header files into Rust data/function declarations
A Love Story
James Munns
@bitshiftmask