Fórum Root.cz
Hlavní témata => Vývoj => Téma založeno: Marekuss 17. 12. 2021, 22:34:04
-
Ahojte kolegovia,
Tentokrat sa na Vas obraciam kvoli serializacii/deserializacii struktur(y) - jednoduchych MSG (TCP/IP) medzi serverom(Rust) a klientom C/freeRTOS(32bit MCU, 128kb RAM).
Spravy su vsehovsudy jednoduche, nieje nutne ich balit do msgpack, protobuf, bson a pod.
Na serializaciu/deserializaciu som pouzil serde/bincode, s tym ze vysledna sprava sa sklada z MessageHeader + MessageBody => Message. Na tomto designe sa mi nepacia dve veci, a to:
MessageHeader::message_type je ulozena hodnota z MessageBodyType::{Version, BatteryHealth} a sucastne Message::body obsahuje {enum MessageBody::Version(Version), enum BatteryHealth(BatteryHealth) }
co ma za nasledok ze Serde serializuje Message(Version) nasledovne
0 1 2 3 4
[message_type | _pad0 | body_size ]
[ crc ]
[ enum ] <---- serde vlozil enum
[x1 | x2 | x3 ]
Takze "struct MessageHeader" ma dlzku 8 bajtov, "struct Version" ma dlzku 4 bajty => idealne by "struct Message" mal mat 12 bajtov.
V skutocnosti ma "struct Message" 16 bajtov, a to z dovodu ze serde prida 4 bajty na rozlisenie enum MessageBody::{Version, BatteryHealth}
Serializovany objekt s prihladnutim na kod vyzsie vyzera nasledovne, kde 11/22 je MessageBodyType::{Version, BatteryHealth}, a 0/1 na 9 pozicii je "enum MessageBody::{Version(), BatteryHealth()}"
serialized_version: (16) vec![11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]
serialized_battery: (16) vec![22, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, ...]
Preto sa pytam skusenejsich, akceptovat to, ze serde prida 4bajty na rozlisenie enumu a tuto vec zohladit na strane klienta,
Pripadne, napada Vas moznost ako namodelovat kod nizsie, tak, aby serde/bincode serializovalo/deserializovalo iba mne chcenych 12bajtov (MessageHeader + MessageBody)
AD: Pokusal som sa namodelovat "pub fn deserialize<'a>(bytes: &'a [u8]) -> Box<MessageTrait>", toto sa mi nepodarilo skrz (https://doc.rust-lang.org/error-index.html#E0038)
Rust:
cargo - serde
cargo - bincode
use serde_repr::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub enum MessageBody {
Version(Version),
BatteryHealth(BatteryHealth),
}
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug)]
#[repr(u8)]
pub enum MessageBodyType {
Version = 11,
BatteryHealth = 22,
}
impl MessageBody {
pub fn value(&self) -> MessageBodyType {
match *self {
MessageBody::Version(_) => MessageBodyType::Version,
MessageBody::BatteryHealth(_) => MessageBodyType::BatteryHealth
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[repr(C)]
pub struct MessageHeader {
message_type: MessageBodyType, // 1
_pad0: u8, // 1
body_size: u16, // 2
crc: u32, // 4
}
impl MessageHeader {
pub fn new_as(message_type: MessageBodyType) -> MessageHeader {
MessageHeader {
message_type: message_type,
_pad0: 0,
body_size: 0,
crc: 0,
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Message {
header: MessageHeader,
body: MessageBody,
}
impl Message {
pub fn serialize(mt: MessageBody) -> Vec<u8> {
let header = MessageHeader::new_as(mt.value());
let header_size_of = std::mem::size_of::<MessageHeader>();
let header_vec = bincode::serialize(&header).unwrap();
let header_vec_len = header_vec.len();
let body_version_size_of = std::mem::size_of::<Version>();
let body_vec: Vec<u8> = bincode::serialize(&mt).unwrap();
let body_vec_vec_len = body_vec.len();
let message: Message = Message { header: header, body: mt };
let message_size_of = std::mem::size_of::<Message>();
let message_vec = bincode::serialize(&message).unwrap();
let message_len = message_vec.len();
message_vec
}
pub fn deserialize<'a>(bytes: &'a [u8]) -> Message {
let message_header_len = std::mem::size_of::<MessageHeader>();
let header_slice: &[u8] = &bytes[0..message_header_len];
let header: MessageHeader = bincode::deserialize(header_slice).unwrap();
let message: Message = bincode::deserialize(bytes).unwrap();
message
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[repr(C)]
pub struct Version {
pub x1: u8, // 1
pub x2: u8, // 1
pub x3: u16, // 2
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[repr(C)]
pub struct BatteryHealth {
pub x1: u8, // 1
pub x2: u8, // 1
pub x3: u16, // 2
}
fn main () -> Result<(), Box<(dyn std::error::Error + 'static)>> {
let serialized_version: Vec<u8> = Message::serialize(MessageBody::Version(Version{x1: 3, x2: 6, x3: 0xFAFA}));
let serialized_battery: Vec<u8> = Message::serialize(MessageBody::BatteryHealth(BatteryHealth{x1: 10, x2: 20, x3: 0x0A0A}));
let deserialized_version: Message = Message::deserialize(&serialized_version);
Ok(())
}
Diky M.
-
Dekuji za potvrzeni, ze rust neni neco co by melo hardcore/lowlevel C programatory zajimat :)
Tolik s**ni s tim, a jeste si to dela co chce, a ne co chci ja mi za to fakt nestoji.
-
Možná pro to bude nějaký dobrý důvod, ale ten kód mi přijde příliš komplikovaný. MessageBodyType je již informace obsažená v MessageBody. Pokud se zbavíme MessageBodyType, zbývá snad už jen jeden problém, a to, že ten tag zabírá 4B a ne jen 1B. IIUC, to by mělo jít nastavit vhodným nastavením serde: https://stackoverflow.com/a/64512528
Drawback samozřejmě bude zpětná nekompatibilita, pokud by ten enum měl přes 256 variant. To plyne ze způsobu kódování. Pak by se jeho tag nevlezl do u8, a byla by to komplikace. Samozřejmě by to šlo řešit ála UTF-8, mít 256 variant enumu, z toho jedna bude Other, která bude kódovat ty zbývající. Hlavní by ale bylo si na to vzpomenout při přidání 256. varianty. Pokud si na to vzpomenete u 257. varianty, bude už z hlediska kompatibility pozdě.
-
Aha, tak s tím řešením délky tagu si nejsem jistý, to možná ořeže ten enum do podoby bez dalších dat. Ale nevím, až tak sběhlý v tom nejsem.
-
Dekuji za potvrzeni, ze rust neni neco co by melo hardcore/lowlevel C programatory zajimat :)
Tolik s**ni s tim, a jeste si to dela co chce, a ne co chci ja mi za to fakt nestoji.
Ukaž, jak bys to udělal v C a že v Rustu to nejde. Rust má i normální "hloupý" union (untagged). OP použil tagged union a vadí mu, že vysokoúrovňový serializační mechanismus ho zachová v pořádku se vším všudy. Ale jinak jasně, pokud Ti to za to nestojí, nezabývej se tím.
-
Dekuji za potvrzeni, ze rust neni neco co by melo hardcore/lowlevel C programatory zajimat :)
Tolik s**ni s tim, a jeste si to dela co chce, a ne co chci ja mi za to fakt nestoji.
Ukaž, jak bys to udělal v C a že v Rustu to nejde. Rust má i normální "hloupý" union (untagged). OP použil tagged union a vadí mu, že vysokoúrovňový serializační mechanismus ho zachová v pořádku se vším všudy. Ale jinak jasně, pokud Ti to za to nestojí, nezabývej se tím.
Jako co na tom chces probuh resit?
typedef struct / union, + __packed__ atribut, pro exaktni definici zpravy
(uint8_t*)&message, pro pretypovani na byte array a sup s tim nekam, pomoci sizeof(message)
Dnesni programatori fakt nemaji tuseni o fyzickem ukladani struktur? Jako taky me prekvapilo u borce co masti Xcode a apple appky, ze nedokazal nastavit bitovy flag popsany v specifikaci - mezi osmy bajty, pomoci jednoducheho msg[0] |= 0x40.
-
Je bincode vůbec pro C? Slyšel jsem že se možná něco plánuje v C++, ale to je tak vše.
-
Bych na to možná reagoval, ale úplně nechci z diskuze o řešení serializace v Rustu dělat vlákno Rust vs. C.
-
Bych na to možná reagoval, ale úplně nechci z diskuze o řešení serializace v Rustu dělat vlákno Rust vs. C.
To je tu celkem normální a v tomto případě asi i přínosné.
-
Normální neznamená, že je to dobře. Ale OK, tady je to takové hraniční. Vidím trochu potenciál k flamewar.
Ono by se něco podobného jako v C dalo udělat i v Rustu. Je to ale za cenu raw unionů, transmute a unsafe. Pak se na libovolnou strukturu lze dívat jako na [u8]. Tím ale v podstatě z Rustu stává tak trochu C, a přináší to nevýhody s tím spojené:
* Výstup bude fixed-length.
* Pokud budou různé varianty unionu různě dlouhé, je tu asi riziko neinicializované paměti a úniku dat.
* Nepůjde použít pokročilejší (de)serializaci. V případě referencí výsledek asi nebude žádoucí…
* Little vs. big endian
* Nic nekontroluje validitu dat; pokud enum má dvě platné hodnoty, může dostat něco úplně jiného. Toto zavání nedefinovaným chováním.
Jinými slovy, i v Rustu to můžete udělat jako v Céčku, ale pak to máte jako v Céčku se vším všudy.
-
Ano, i když se dá Rust použít na stejné věci jako C, neznamená to že je to »vylepšené a bezpečnější C«. Člověk musí změnit svůj způsob myšlení.
-
Normální neznamená, že je to dobře. Ale OK, tady je to takové hraniční. Vidím trochu potenciál k flamewar.
Ono by se něco podobného jako v C dalo udělat i v Rustu. Je to ale za cenu raw unionů, transmute a unsafe. Pak se na libovolnou strukturu lze dívat jako na [u8]. Tím ale v podstatě z Rustu stává tak trochu C, a přináší to nevýhody s tím spojené:
* Výstup bude fixed-length.
* Pokud budou různé varianty unionu různě dlouhé, je tu asi riziko neinicializované paměti a úniku dat.
* Nepůjde použít pokročilejší (de)serializaci. V případě referencí výsledek asi nebude žádoucí…
* Little vs. big endian
* Nic nekontroluje validitu dat; pokud enum má dvě platné hodnoty, může dostat něco úplně jiného. Toto zavání nedefinovaným chováním.
Jinými slovy, i v Rustu to můžete udělat jako v Céčku, ale pak to máte jako v Céčku se vším všudy.
Když je nutné použít transmute a unsafe, je lepší sáhnout přímo po C.
-
ja by som nevysmyslal koleso a pouzil flatbuffers.
-
Ukaž, jak bys to udělal v C a že v Rustu to nejde. Rust má i normální "hloupý" union (untagged). OP použil tagged union a vadí mu, že vysokoúrovňový serializační mechanismus ho zachová v pořádku se vším všudy. Ale jinak jasně, pokud Ti to za to nestojí, nezabývej se tím.
OP to nevadi, OP poukazal na fakt ze to tak je. OP by rad vyuzil plne jazyk Rust (kludne aj nizkourovnovu serializaciu (ak mas predstavu ako)) a zaroven sa vyhol unsafe {}.
Vies mi poradit ako serializovat struktury (kludne aj bez serde/bincode), s tym ze vyuzijem plne jazyk Rust "se vsim vsudy", vyhnem sa "unsafe" semantike?
Rad by som sa vyhol flamewar C vs Rust, vsetci tusime ako to zapiseme v C, ako to spravit Safe v Ruste?
-
[ako serializovat struktury (kludne aj bez serde/bincode), s tym ze vyuzijem plne jazyk Rust "se vsim vsudy", vyhnem sa "unsafe" semantike?
Není nejlepší to v tomto případě napsat natvrdo, jeden bajt na typ a pak řešit switchem, co se načte/vytvoří?
-
Když nechceš enum, tak tam dej int :)
-
Ad unsafe – je pravda, že tím dělá člověk z Rustu Céčko. Na druhou stranu, pokud bych měl mít kousek unsafe a zbytek ± idiomatický Rust, stále v tom vidím smysl.
Záleží, co od toho očekáváte. Ona low-level serializace a deserializace v podstatě bez unsafe nejde, protože tam narážíte na problémy, které jsem popsal. (Pravda, část z nich nesouvisí tolik s unsafe, ale stačí jeden, který souvisí.) Bez unsafe je několik možností:
A. Jak jsem psal v tom prvním příspěvku, zbavil bych se nadbytečného fieldu a hledal, jak ten enum tag serializovat do jednoho bytu. Možná bude fungovat to, co jsem poslal. Možná s tím bude problém, že budete serializovat jen ten tag bez dat, ale to.snad zjistíte velmi rychle při pokusu o deserializaci.
B. Ty flatbuffers by taky mohly jít
C. Když všechno ostatní selže, můžete si udělat serializaci a deserializaci posvém.
-
Když nechceš enum, tak tam dej int :)
Babica radí: Když nemáš enum, dej tam int. Kdo nemá int, tak ať tam dá nějaký jiný číselný typ. A teď pozor, důležitý! Kdo nemá žádný typ, ať tam nedává vůbec žádný typ :)
-
Na druhou stranu, pokud bych měl mít kousek unsafe a zbytek ± idiomatický Rust, stále v tom vidím smysl.
Tohle se dělá běžně, i v knihovně Rustu, ono to často ani jinak nejde.
-
Však; chtěl jsem odporovat tvrzení, že pak je lepší sáhnout rovnou po C.
-
chtěl jsem odporovat tvrzení, že pak je lepší sáhnout rovnou po C.
Jistě, pokud je celá aplikace v “safe” Rustu a někde jeden unsafe blok, tak má Rust pořád smysl. IMHO právě na této úrovni má Rust smysl, protože co si budeme povídat, v holém C je snadné nasekat těžko odhalitelné chyby.
-
Hele a nedá se prostě ten enum přes serde serializovat s pomocí "untagged"? Něco jako
use bincode;
use serde::{Serialize, Deserialize};
type Version = u8;
type BatteryHealth = u8;
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum MessageBodyType {
Version(Version),
BatteryHealth(BatteryHealth),
}
fn main() {
let body = MessageBodyType::BatteryHealth(1);
let encoded: Vec<u8> = bincode::serialize(&body).unwrap();
println!("Result {:?}", encoded);
}
-
Spravy su vsehovsudy jednoduche, nieje nutne ich balit do msgpack, protobuf, bson a pod.
ASN.1 BER ?
[/provokace]
(Hihihi :-D a zdrhám až se mi za patami práší, než po mně někdo něco hodí.)
-
ASN.1 BER ?
(Hihihi :-D a zdrhám až se mi za patami práší, než po mně někdo něco hodí.)
+1
Jako me to taky napadlo, ze prece existuji standardy jak neco serializovat, ale to neni pry dostatecne moderni a hranate jako ctvercove kolo ;)
Ani nevim co me to nedavno potkalo, ale musel jsem deserializovat nejaky ASN blob - tak otevrete standard, projdete bit po bitu.. a mate vysledek.
Neco takoveho u tech frikulinskych knihoven asi tezko nastane - jakmile neni implementace, tak si neskrtnete, a bez toho aniz by jste spustili onen kod taky ne.. nechapu co na tom lidi bavi, k ladeni to je uplne peklo.
-
Spravy su vsehovsudy jednoduche, nieje nutne ich balit do msgpack, protobuf, bson a pod.
ASN.1 BER ?
[/provokace]
(Hihihi :-D a zdrhám až se mi za patami práší, než po mně někdo něco hodí.)
Prave naopak, ja sa rad niecomu novemu priucim, nieje dovod vyskakovat z okna :)
Sice netusim, ako moc je Rust hranaty, moderny alebo firkulinsky - to musia zhodnotit lepsi ako ja.
Ma nejaky prinos implementacia ASN.1 BER/TLV a obdobne na strane Rust-u a zaroven v C, ked si budem vymienat spravy o maximalnej dlzke 64bytov? Neviem pytam sa...
-
Sice netusim, ako moc je Rust hranaty, moderny alebo firkulinsky
Je tohle všechno, a to značně :)
-
Sice netusim, ako moc je Rust hranaty, moderny alebo firkulinsky - to musia zhodnotit lepsi ako ja.
Ma nejaky prinos implementacia ASN.1 BER/TLV a obdobne na strane Rust-u a zaroven v C, ked si budem vymienat spravy o maximalnej dlzke 64bytov? Neviem pytam sa...
Jiste ze ma. Jednak je velka sance, ze na strane Rust-u bude ready made knihovna pro tohle formatovani, a na strane C muzes lehce vyjit z binarni sablony, kde budes jenom tocit hodnoty, ale tu omacku kolem typovani a strukturovani budes mit preddefinovanou (use case: par statickych zprav, a dynamicka runtime kompozice bistreamu neni zadouci).
Pokud ono C bude zpravy i dostavat, tak muzes udelat svuj parser na podmnozinu, ktery to muze vzdat, jestli prijde neco jineho.
Otazka taky pak - proc jit do binarni formy, pokud by se jednalo o humanoidni data - v tom pripade bych mozna preferoval i JSON (nebo v extremu pak ukecanejsi XML).
Pridavam i aplikacni dotaz tedy:
Je potreba, aby zprava byla kontrolovatelna zda je well-formed, nebo ta aplikace snese krmeni jakymkoliv binarnim bordelem?
(jako ne, ze by nesla krmit i falesnymi daty v textove podobe.. ale u binarni varianty se to hur kontroluje, kdyz to neni napr v tom ASN.1)
(a pak se uz pomalu dostavame k bezpecnosti, zda by zprava mela byt kryptograficky podepsana)
-
...no já si z dob svého telecího mládí pamatuju nějaké open-source ASN.1 knihovny, kde se tuším generovaly C++ třídy nějakým lexerem z ASN.1 symbolické notace, a pak se to C++ kompilovalo. Něco takového bych do MCU rozhodně cpát nechtěl. Tenkrát kdysi pod Windózama, když jsem si chtěl hrát se SNMP, a nějak jsem nedokázal najít vyhovující portovatelnou jednoduchou knihovnu ASN.1, tak jsem si pár základních datových typů ASN.1 BER napsal v jednoduchém C++ sám. (Ty zdrojáky dávno nemám. Stejně to bylo jenom torzo.)
-
(a pak se uz pomalu dostavame k bezpecnosti, zda by zprava mela byt kryptograficky podepsana)
asi tezko tohle resit u zprav, ktere maji jednotky bytu. Bezpecnost asi resi nejaky stateful protokol nad tim.
-
1. Bincode je formát určený pro Rust, implementace je silně založena na Rustovém typovém systému a implementace pro jiné jazyky (zatím) nejsou. Takže nevím jestli je to dobrý nápad pro komunikaci s C programy.
2. CBOR?
-
OP: Došel jsi prosímtě k něčemu? Zafungoval Ti ten "untagged"?
-
OP: Došel jsi prosímtě k něčemu? Zafungoval Ti ten "untagged"?
Zdar, ano, untagged je nakoniec uzitocne makro :)
Nakolko Rust len skumam, tak som sa nepustal do zbytocnych zlozitosti... vsak sa bude refaktorovat.
Je to +- ok, ked bude cas, tak dvojity matach() blok nahradim makrom, tak isto by slo nahradit aj "pub fn value(&self) -> MessageBodyType" ako makro.. mozno casom
use serde_repr::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug)]
#[serde(untagged)]
#[repr(u8)]
pub enum MessageBodyType {
Version = 11,
BatteryHealth = 22,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub enum MessageBody {
Version(Version),
BatteryHealth(BatteryHealth),
}
impl MessageBody {
pub fn value(&self) -> MessageBodyType {
match *self {
MessageBody::Version(_) => MessageBodyType::Version,
MessageBody::BatteryHealth(_) => MessageBodyType::BatteryHealth
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[repr(C)]
pub struct MessageHeader {
message_type: MessageBodyType, // 1
_pad0: u8, // 1
body_size: u16, // 2
crc: u32, // 4
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Message {
header: MessageHeader,
body: MessageBody,
}
impl Message {
pub fn serialize_body(mb: MessageBody) -> Result<Vec<u8>, Error> {
let mut body_vec: Vec<u8>;
match &mb {
MessageBody::Version(v) => {
body_vec = bincode::serialize(&v)?;
},
MessageBody::BatteryHealth(h) => {
body_vec = bincode::serialize(&h)?;
}
}
let header : MessageHeader = MessageHeader {
message_type: mb.value(),
_pad0: HEADER_PAD,
body_size: body_vec.len() as u32,
crc: 0,
};
let mut header_vec = bincode::serialize(&header)?;
header_vec.append(&mut body_vec);
Ok(header_vec)
}
pub fn deserialize_body<'a>(bytes: &'a [u8]) -> Result<MessageBody, Error> {
if bytes.len() < std::mem::size_of::<MessageHeader>(){
return Err(());
}
let message_header_len = std::mem::size_of::<MessageHeader>();
match header.message_type {
MessageBodyType::Version => {
let version: Version = bincode::deserialize(&bytes[message_header_len..bytes.len()])?;
return Ok(MessageBody::Version(version));
},
MessageBodyType::BatteryHealth => {
let battery: BatteryHealth = bincode::deserialize(&bytes[message_header_len..bytes.len()])?;
return Ok(MessageBody::BatteryHealth(battery));
}
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[repr(C)]
pub struct Version {
pub x1: u8, // 1
pub x2: u8, // 1
pub x3: u16, // 2
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[repr(C)]
pub struct BatteryHealth {
pub x1: u8, // 1
pub x2: u8, // 1
pub x3: u16, // 2
}
fn main () -> Result<(), Box<(dyn std::error::Error + 'static)>> {
let serialized_version: Vec<u8> = Message::serialize(MessageBody::Version(Version{x1: 3, x2: 6, x3: 0xFAFA})).unwrap();
let serialized_battery: Vec<u8> = Message::serialize(MessageBody::BatteryHealth(BatteryHealth{x1: 10, x2: 20, x3: 0x0A0A})).unwrap();
let deserialized_version: MessageBody = Message::deserialize(&serialized_version).unwrap();
Ok(())
}