From 9225e10af7edb83821e91f52ec6418918d5000b0 Mon Sep 17 00:00:00 2001 From: Joao Mario Lago Date: Wed, 3 Dec 2025 13:36:37 -0300 Subject: [PATCH 1/2] crates:generators:c: Add basic C generation * Add generator capable of converting blueberry_full.idl * Add basic in place parsing/packing --- crates/generators/c/src/lib.rs | 1052 ++++++++++++++++++++------------ 1 file changed, 677 insertions(+), 375 deletions(-) diff --git a/crates/generators/c/src/lib.rs b/crates/generators/c/src/lib.rs index 97d24d5..1dfb842 100644 --- a/crates/generators/c/src/lib.rs +++ b/crates/generators/c/src/lib.rs @@ -1,52 +1,166 @@ -use blueberry_ast::Definition; +use blueberry_ast::{ + Annotation, AnnotationParam, Commented, ConstDef, ConstValue, Definition, EnumDef, MessageDef, + StructDef, Type, TypeDef, +}; use blueberry_codegen_core::{ - CodegenError, GeneratedFile, MESSAGE_HEADER_SIZE, MessageSpec, PrimitiveType, TopicFormat, - TopicPlaceholder, collect_messages, snake_case_path, topic_format, uppercase_path, + CodegenError, DEFAULT_MODULE_KEY, GeneratedFile, MESSAGE_HEADER_SIZE, ResolvedMember, + TypeRegistry, class_name, quoted_string, uppercase_path, }; use genco::lang::c::Tokens; use genco::quote; -const OUTPUT_PATH: &str = "c/messages.h"; -const RUNTIME_PATH: &str = "c/runtime.h"; -const TOPIC_BUFFER_LEN: usize = 256; +const RUNTIME_HEADER_PATH: &str = "c/blueberry_runtime.h"; +const RUNTIME_SOURCE_PATH: &str = "c/blueberry_runtime.c"; +const DEFINITIONS_HEADER_PATH: &str = "c/blueberry_messages.h"; +const DEFINITIONS_SOURCE_PATH: &str = "c/blueberry_messages.c"; +/// Generate the C runtime and data structures for the provided IDL definitions. pub fn generate(definitions: &[Definition]) -> Result, CodegenError> { - let messages = collect_messages(definitions)?; - if messages.is_empty() { - return Ok(Vec::new()); - } - let generator = CGenerator::new(&messages); + let mut generator = CGenerator::new(definitions); + let runtime_header = CGenerator::render_runtime_header(); + let runtime_source = CGenerator::render_runtime_source(); + let header = generator.render_messages_header(definitions)?; + let source = generator.render_messages_source(); Ok(vec![ GeneratedFile { - path: RUNTIME_PATH.to_string(), - contents: generator.render_runtime(), + path: RUNTIME_HEADER_PATH.to_string(), + contents: runtime_header, + }, + GeneratedFile { + path: RUNTIME_SOURCE_PATH.to_string(), + contents: runtime_source, + }, + GeneratedFile { + path: DEFINITIONS_HEADER_PATH.to_string(), + contents: header, }, GeneratedFile { - path: OUTPUT_PATH.to_string(), - contents: generator.render_messages(), + path: DEFINITIONS_SOURCE_PATH.to_string(), + contents: source, }, ]) } -struct CGenerator<'a> { - messages: &'a [MessageSpec], +struct CGenerator { + registry: TypeRegistry, + message_topics: Vec, } -impl<'a> CGenerator<'a> { - fn new(messages: &'a [MessageSpec]) -> Self { - Self { messages } +struct MessageTopic { + ident: String, + literal: String, +} + +impl CGenerator { + fn new(definitions: &[Definition]) -> Self { + Self { + registry: TypeRegistry::new(definitions), + message_topics: Vec::new(), + } + } + + fn render_messages_header( + &mut self, + definitions: &[Definition], + ) -> Result { + self.message_topics.clear(); + let mut key_gen = MessageKeyGen::default(); + let definitions_tokens = self.emit_definitions( + definitions, + &mut Vec::new(), + DEFAULT_MODULE_KEY, + &mut key_gen, + )?; + let tokens: Tokens = quote! { + #ifndef BLUEBERRY_MESSAGES_H + #define BLUEBERRY_MESSAGES_H + + #include "blueberry_runtime.h" + + #ifdef __cplusplus + extern "C" { + #endif + + $(for section in definitions_tokens => + $section + + ) + + #ifdef __cplusplus + } // extern "C" + #endif + + #endif /* BLUEBERRY_MESSAGES_H */ + }; + Ok(tokens.to_file_string().expect("render blueberry c header")) + } + + fn render_messages_source(&self) -> String { + let topics: Vec = self + .message_topics + .iter() + .map(|topic| { + let ident = &topic.ident; + let literal = &topic.literal; + quote!(const char $ident[] = $literal; $['\n']) + }) + .collect(); + let tokens: Tokens = quote! { + #include "blueberry_messages.h" + #include + + $(for entry in topics => + $entry + + ) + }; + tokens.to_file_string().expect("render blueberry c source") } - fn render_runtime(&self) -> String { - let helpers = helpers_tokens(); + fn render_runtime_header() -> String { + let runtime_tokens = Self::runtime_header_tokens(); let tokens: Tokens = quote! { - #pragma once + #ifndef BLUEBERRY_RUNTIME_H + #define BLUEBERRY_RUNTIME_H + #include #include #include #include - #define BLUEBERRY_MESSAGE_HEADER_SIZE $(MESSAGE_HEADER_SIZE)u + #ifdef __cplusplus + extern "C" { + #endif + + $runtime_tokens + + #ifdef __cplusplus + } // extern "C" + #endif + + #endif /* BLUEBERRY_RUNTIME_H */ + }; + tokens + .to_file_string() + .expect("render blueberry runtime header") + } + + fn render_runtime_source() -> String { + let runtime_tokens = Self::runtime_source_tokens(); + let tokens: Tokens = quote! { + #include "blueberry_runtime.h" + + $runtime_tokens + }; + tokens + .to_file_string() + .expect("render blueberry runtime source") + } + + fn runtime_header_tokens() -> Tokens { + let header_size_literal = format!("((size_t){})", MESSAGE_HEADER_SIZE); + quote! { + #define BLUEBERRY_MESSAGE_HEADER_SIZE $header_size_literal typedef struct { uint16_t payload_words; @@ -55,426 +169,614 @@ impl<'a> CGenerator<'a> { uint16_t message_key; } blueberry_message_header_t; - $helpers - }; - tokens.to_file_string().expect("render runtime") - } + bool blueberry_message_header_parse( + const uint8_t *frame, + size_t frame_len, + blueberry_message_header_t *out_header); - fn render_messages(&self) -> String { - let message_blocks: Vec = - self.messages.iter().map(|m| self.emit_message(m)).collect(); + bool blueberry_message_header_write( + uint8_t *frame, + size_t frame_len, + const blueberry_message_header_t *header); - let mut tokens: Tokens = quote! { - #pragma once + static inline size_t blueberry_align4(size_t value) { + return (value + 3u) & ~((size_t)3u); + } - #include - #include - #include - #include "runtime.h" + static inline uint16_t blueberry_payload_words_from_length(size_t length) { + size_t aligned = blueberry_align4(length); + if (aligned == 0u) { + return 0u; + } + return (uint16_t)(aligned / 4u); + } - #define BLUEBERRY_MAX_TOPIC_LENGTH $(TOPIC_BUFFER_LEN)u + static inline size_t blueberry_payload_length_from_words(uint16_t words) { + return (size_t)words * 4u; + } - typedef int (*blueberry_publish_fn)(const char *topic, const uint8_t *frame, size_t frame_len, void *user_data); - }; + static inline size_t blueberry_frame_size_from_payload(size_t length) { + return BLUEBERRY_MESSAGE_HEADER_SIZE + blueberry_align4(length); + } - for (idx, block) in message_blocks.iter().enumerate() { - if idx > 0 { - tokens.push(); - tokens.push(); - } else { - tokens.push(); + static inline uint8_t blueberry_read_u8(const uint8_t *ptr) { + return ptr[0]; } - tokens.extend(block.clone()); - } - tokens.to_file_string().expect("render messages") - } + static inline void blueberry_write_u8(uint8_t *ptr, uint8_t value) { + ptr[0] = value; + } - fn emit_message(&self, message: &MessageSpec) -> Tokens { - let snake = snake_case_path(&message.scope, &message.name); - let struct_ident = format!("blueberry_{}_t", snake); - let struct_macro = format!( - "{}_STRUCT_SIZE", - uppercase_path(&message.scope, &message.name) - ); - let padded_macro = format!( - "{}_PADDED_SIZE", - uppercase_path(&message.scope, &message.name) - ); - let payload_macro = format!( - "{}_PAYLOAD_WORDS", - uppercase_path(&message.scope, &message.name) - ); - let module_macro = format!( - "{}_MODULE_KEY", - uppercase_path(&message.scope, &message.name) - ); - let message_macro = format!( - "{}_MESSAGE_KEY", - uppercase_path(&message.scope, &message.name) - ); - let topic_macro = format!( - "{}_TOPIC_FMT", - uppercase_path(&message.scope, &message.name) - ); - let topic = topic_format(&message.topic); - let topic_literal = blueberry_codegen_core::quoted_string(&topic.template); - let struct_ident_q = struct_ident.clone(); - let struct_macro_q = struct_macro.clone(); - let padded_macro_q = padded_macro.clone(); - let payload_macro_q = payload_macro.clone(); - let module_macro_q = module_macro.clone(); - let message_macro_q = message_macro.clone(); - let topic_macro_q = topic_macro.clone(); - - let pack = self.emit_pack(message, &struct_ident, &padded_macro); - let parse = self.emit_parse(message, &struct_ident, &struct_macro); - let from_frame = - self.emit_from_frame(message, &struct_ident, &module_macro, &message_macro); - let callback = self.emit_callback(message, &struct_ident); - let publish = self.emit_publish( - message, - &struct_ident, - &padded_macro, - &payload_macro, - &module_macro, - &message_macro, - &topic, - ); + static inline uint16_t blueberry_read_u16(const uint8_t *ptr) { + return (uint16_t)ptr[0] | ((uint16_t)ptr[1] << 8); + } - quote! { - typedef struct { - $(for field in &message.fields => - $(c_field_type(field.primitive)) $(field.name.clone()); - ) - } $struct_ident_q; + static inline void blueberry_write_u16(uint8_t *ptr, uint16_t value) { + ptr[0] = (uint8_t)(value & 0xffu); + ptr[1] = (uint8_t)((value >> 8) & 0xffu); + } - enum { - $struct_macro_q = $(message.field_payload_size()), - $padded_macro_q = $(message.padded_payload_size()), - $payload_macro_q = $(message.payload_words()), - $module_macro_q = $(message.module_key), - $message_macro_q = $(message.message_key) - }; + static inline uint32_t blueberry_read_u32(const uint8_t *ptr) { + return (uint32_t)ptr[0] + | ((uint32_t)ptr[1] << 8) + | ((uint32_t)ptr[2] << 16) + | ((uint32_t)ptr[3] << 24); + } - static const char $topic_macro_q[] = $topic_literal; + static inline void blueberry_write_u32(uint8_t *ptr, uint32_t value) { + ptr[0] = (uint8_t)(value & 0xffu); + ptr[1] = (uint8_t)((value >> 8) & 0xffu); + ptr[2] = (uint8_t)((value >> 16) & 0xffu); + ptr[3] = (uint8_t)((value >> 24) & 0xffu); + } - $pack + static inline uint64_t blueberry_read_u64(const uint8_t *ptr) { + uint64_t value = 0u; + value |= (uint64_t)ptr[0]; + value |= ((uint64_t)ptr[1] << 8); + value |= ((uint64_t)ptr[2] << 16); + value |= ((uint64_t)ptr[3] << 24); + value |= ((uint64_t)ptr[4] << 32); + value |= ((uint64_t)ptr[5] << 40); + value |= ((uint64_t)ptr[6] << 48); + value |= ((uint64_t)ptr[7] << 56); + return value; + } - $parse + static inline void blueberry_write_u64(uint8_t *ptr, uint64_t value) { + ptr[0] = (uint8_t)(value & 0xffu); + ptr[1] = (uint8_t)((value >> 8) & 0xffu); + ptr[2] = (uint8_t)((value >> 16) & 0xffu); + ptr[3] = (uint8_t)((value >> 24) & 0xffu); + ptr[4] = (uint8_t)((value >> 32) & 0xffu); + ptr[5] = (uint8_t)((value >> 40) & 0xffu); + ptr[6] = (uint8_t)((value >> 48) & 0xffu); + ptr[7] = (uint8_t)((value >> 56) & 0xffu); + } - $from_frame + static inline bool blueberry_read_bool(const uint8_t *ptr) { + return blueberry_read_u8(ptr) != 0u; + } - $callback + static inline void blueberry_write_bool(uint8_t *ptr, bool value) { + blueberry_write_u8(ptr, value ? 1u : 0u); + } - $publish - } - } + static inline int8_t blueberry_read_char(const uint8_t *ptr) { + uint8_t raw = blueberry_read_u8(ptr); + int8_t value; + memcpy(&value, &raw, sizeof(value)); + return value; + } - fn emit_pack(&self, message: &MessageSpec, struct_ident: &str, padded_macro: &str) -> Tokens { - let pack_fn = format!("{}_pack", snake_case_path(&message.scope, &message.name)); - let writes: Tokens = quote! { - $(for field in &message.fields => - $(c_writer(field.primitive))(msg->$(field.name.clone()), payload + offset); - offset += $(field.primitive.size()); - ) - }; + static inline void blueberry_write_char(uint8_t *ptr, int8_t value) { + uint8_t raw; + memcpy(&raw, &value, sizeof(raw)); + blueberry_write_u8(ptr, raw); + } - quote! { - static inline void $pack_fn(const $struct_ident *msg, uint8_t *payload) { - memset(payload, 0, $padded_macro); - size_t offset = 0; - $writes + static inline int16_t blueberry_read_i16(const uint8_t *ptr) { + uint16_t raw = blueberry_read_u16(ptr); + int16_t value; + memcpy(&value, &raw, sizeof(value)); + return value; } - } - } - fn emit_parse(&self, message: &MessageSpec, struct_ident: &str, struct_macro: &str) -> Tokens { - let parse_fn = format!("{}_parse", snake_case_path(&message.scope, &message.name)); - let reads: Tokens = quote! { - $(for field in &message.fields => - out->$(field.name.clone()) = $(c_reader(field.primitive))(payload + offset); - offset += $(field.primitive.size()); - ) - }; + static inline void blueberry_write_i16(uint8_t *ptr, int16_t value) { + uint16_t raw; + memcpy(&raw, &value, sizeof(raw)); + blueberry_write_u16(ptr, raw); + } - quote! { - static inline int $parse_fn(const uint8_t *payload, size_t payload_len, $struct_ident *out) { - if (payload_len < $struct_macro) { return -1; } - size_t offset = 0; - $reads - return 0; + static inline int32_t blueberry_read_i32(const uint8_t *ptr) { + uint32_t raw = blueberry_read_u32(ptr); + int32_t value; + memcpy(&value, &raw, sizeof(value)); + return value; } - } - } - fn emit_from_frame( - &self, - message: &MessageSpec, - struct_ident: &str, - module_macro: &str, - message_macro: &str, - ) -> Tokens { - let snake_name = snake_case_path(&message.scope, &message.name); - let from_frame_fn = format!("{}_from_frame", snake_name); - let parse_fn = format!("{}_parse", snake_name); + static inline void blueberry_write_i32(uint8_t *ptr, int32_t value) { + uint32_t raw; + memcpy(&raw, &value, sizeof(raw)); + blueberry_write_u32(ptr, raw); + } - quote! { - static inline int $from_frame_fn(const uint8_t *frame, size_t frame_len, $struct_ident *out) { - blueberry_message_header_t header; - if (blueberry_read_header(frame, frame_len, &header) != 0) { return -1; } - if (header.module_key != $module_macro || header.message_key != $message_macro) { return -2; } - size_t payload_len = (size_t)header.payload_words * 4u; - if (frame_len < BLUEBERRY_MESSAGE_HEADER_SIZE + payload_len) { return -3; } - return $parse_fn(frame + BLUEBERRY_MESSAGE_HEADER_SIZE, payload_len, out); + static inline int64_t blueberry_read_i64(const uint8_t *ptr) { + uint64_t raw = blueberry_read_u64(ptr); + int64_t value; + memcpy(&value, &raw, sizeof(value)); + return value; } - } - } - fn emit_callback(&self, message: &MessageSpec, struct_ident: &str) -> Tokens { - let snake_name = snake_case_path(&message.scope, &message.name); - let callback_ident = format!("{}_callback", snake_name); - let callback_ident_q = callback_ident.clone(); - let dispatch_fn = format!("{}_dispatch", snake_name); - let from_frame_fn = format!("{}_from_frame", snake_name); + static inline void blueberry_write_i64(uint8_t *ptr, int64_t value) { + uint64_t raw; + memcpy(&raw, &value, sizeof(raw)); + blueberry_write_u64(ptr, raw); + } - quote! { - typedef void (*$callback_ident_q)(const $struct_ident *msg, void *user_data); + static inline float blueberry_read_f32(const uint8_t *ptr) { + uint32_t raw = blueberry_read_u32(ptr); + float value; + memcpy(&value, &raw, sizeof(value)); + return value; + } - static inline void $dispatch_fn(const uint8_t *frame, size_t frame_len, $callback_ident callback, void *user_data) { - $struct_ident msg; - if ($from_frame_fn(frame, frame_len, &msg) == 0) { callback(&msg, user_data); } + static inline void blueberry_write_f32(uint8_t *ptr, float value) { + uint32_t raw; + memcpy(&raw, &value, sizeof(raw)); + blueberry_write_u32(ptr, raw); + } + + static inline double blueberry_read_f64(const uint8_t *ptr) { + uint64_t raw = blueberry_read_u64(ptr); + double value; + memcpy(&value, &raw, sizeof(value)); + return value; + } + + static inline void blueberry_write_f64(uint8_t *ptr, double value) { + uint64_t raw; + memcpy(&raw, &value, sizeof(raw)); + blueberry_write_u64(ptr, raw); } } } - #[allow(clippy::too_many_arguments)] - fn emit_publish( - &self, - message: &MessageSpec, - struct_ident: &str, - padded_macro: &str, - payload_macro: &str, - module_macro: &str, - message_macro: &str, - topic: &TopicFormat, - ) -> Tokens { - let snake_name = snake_case_path(&message.scope, &message.name); - let publish_fn = format!("{}_publish", snake_name); - let pack_fn = format!("{}_pack", snake_name); - let fmt_literal = format!("\"{}\"", topic.template); - let fmt_args = c_format_arguments(&topic.placeholders); - - let args_branch: Tokens = if fmt_args.is_empty() { - quote!(int topic_len = snprintf(topic, sizeof(topic), $fmt_literal);) - } else { - quote!(int topic_len = snprintf(topic, sizeof(topic), $fmt_literal, $(for arg in &fmt_args join (, ) => $arg););) - }; - + fn runtime_source_tokens() -> Tokens { quote! { - static inline int $publish_fn(blueberry_publish_fn publish, void *user_data, const char *device_type, const char *nid, const $struct_ident *msg) { - uint8_t frame[BLUEBERRY_MESSAGE_HEADER_SIZE + $padded_macro] = {0}; - blueberry_message_header_t header = { - .payload_words = $payload_macro, - .flags = 0, - .module_key = $module_macro, - .message_key = $message_macro, - }; - blueberry_write_header(&header, frame); - $pack_fn(msg, frame + BLUEBERRY_MESSAGE_HEADER_SIZE); - char topic[BLUEBERRY_MAX_TOPIC_LENGTH]; - $args_branch - if (topic_len < 0 || (size_t)topic_len >= sizeof(topic)) { return -4; } - return publish(topic, frame, sizeof(frame), user_data); + bool blueberry_message_header_parse( + const uint8_t *frame, + size_t frame_len, + blueberry_message_header_t *out_header) { + if (frame == NULL || out_header == NULL) { + return false; + } + if (frame_len < BLUEBERRY_MESSAGE_HEADER_SIZE) { + return false; + } + out_header->payload_words = blueberry_read_u16(frame); + out_header->flags = blueberry_read_u16(frame + 2); + out_header->module_key = blueberry_read_u16(frame + 4); + out_header->message_key = blueberry_read_u16(frame + 6); + return true; + } + + bool blueberry_message_header_write( + uint8_t *frame, + size_t frame_len, + const blueberry_message_header_t *header) { + if (frame == NULL || header == NULL) { + return false; + } + if (frame_len < BLUEBERRY_MESSAGE_HEADER_SIZE) { + return false; + } + blueberry_write_u16(frame, header->payload_words); + blueberry_write_u16(frame + 2, header->flags); + blueberry_write_u16(frame + 4, header->module_key); + blueberry_write_u16(frame + 6, header->message_key); + return true; } } } -} -fn c_writer(primitive: PrimitiveType) -> Tokens { - match primitive { - PrimitiveType::Bool | PrimitiveType::Char | PrimitiveType::Octet => { - quote!(blueberry_write_u8) + fn emit_definitions( + &mut self, + defs: &[Definition], + scope: &mut Vec, + module_key: u16, + key_gen: &mut MessageKeyGen, + ) -> Result, CodegenError> { + let mut out = Vec::new(); + for def in defs { + match def { + Definition::ModuleDef(module_def) => { + let module_key = + annotation_u16(&module_def.annotations, "module_key").unwrap_or(module_key); + scope.push(module_def.node.name.clone()); + let nested = self.emit_definitions( + &module_def.node.definitions, + scope, + module_key, + key_gen, + )?; + scope.pop(); + out.extend(nested); + } + Definition::EnumDef(enum_def) => { + let enum_tokens = self.emit_enum(enum_def, scope); + out.push(quote! { + $enum_tokens + + }); + } + Definition::StructDef(struct_def) => { + let struct_tokens = self.emit_struct(struct_def, scope); + out.push(quote! { + $struct_tokens + + }); + } + Definition::MessageDef(message_def) => { + let message_tokens = + self.emit_message(message_def, scope, module_key, key_gen)?; + out.push(quote! { + $message_tokens + + }); + } + Definition::ConstDef(const_def) => { + let const_tokens = self.emit_const(const_def, scope); + out.push(quote! { + $const_tokens + + }); + } + Definition::TypeDef(typedef_def) => { + let typedef_tokens = self.emit_typedef(typedef_def, scope); + out.push(quote! { + $typedef_tokens + + }); + } + Definition::ImportDef(_) => {} + } } - PrimitiveType::I16 => quote!(blueberry_write_i16), - PrimitiveType::U16 => quote!(blueberry_write_u16), - PrimitiveType::I32 => quote!(blueberry_write_i32), - PrimitiveType::U32 => quote!(blueberry_write_u32), - PrimitiveType::I64 => quote!(blueberry_write_i64), - PrimitiveType::U64 => quote!(blueberry_write_u64), - PrimitiveType::F32 => quote!(blueberry_write_f32), - PrimitiveType::F64 => quote!(blueberry_write_f64), + Ok(out) } -} -fn c_reader(primitive: PrimitiveType) -> Tokens { - match primitive { - PrimitiveType::Bool | PrimitiveType::Char | PrimitiveType::Octet => { - quote!(blueberry_read_u8) + fn emit_enum(&self, enum_def: &Commented, scope: &[String]) -> Tokens { + let name = class_name(scope, &enum_def.node.name); + let enumerators: Vec = enum_def + .node + .enumerators + .iter() + .map(|member| { + if let Some(value) = &member.value { + let literal = Self::const_literal(value); + quote!( $(member.name.clone()) = $literal ) + } else { + quote!( $(member.name.clone()) ) + } + }) + .collect(); + quote! { + typedef enum { + $(for value in enumerators join (,$['\n']) => $value) + } $name; + + $['\n'] } - PrimitiveType::I16 => quote!(blueberry_read_i16), - PrimitiveType::U16 => quote!(blueberry_read_u16), - PrimitiveType::I32 => quote!(blueberry_read_i32), - PrimitiveType::U32 => quote!(blueberry_read_u32), - PrimitiveType::I64 => quote!(blueberry_read_i64), - PrimitiveType::U64 => quote!(blueberry_read_u64), - PrimitiveType::F32 => quote!(blueberry_read_f32), - PrimitiveType::F64 => quote!(blueberry_read_f64), } -} -fn c_field_type(primitive: PrimitiveType) -> Tokens { - match primitive { - PrimitiveType::Bool => quote!(uint8_t), - PrimitiveType::Char => quote!(char), - PrimitiveType::Octet => quote!(uint8_t), - PrimitiveType::I16 => quote!(int16_t), - PrimitiveType::U16 => quote!(uint16_t), - PrimitiveType::I32 => quote!(int32_t), - PrimitiveType::U32 => quote!(uint32_t), - PrimitiveType::I64 => quote!(int64_t), - PrimitiveType::U64 => quote!(uint64_t), - PrimitiveType::F32 => quote!(float), - PrimitiveType::F64 => quote!(double), + fn emit_struct(&self, struct_def: &Commented, scope: &[String]) -> Tokens { + let mut path = scope.to_vec(); + path.push(struct_def.node.name.clone()); + let members = self.registry.collect_struct_members(&path); + self.render_struct_body(&class_name(scope, &struct_def.node.name), &members, scope) } -} -fn c_format_arguments(placeholders: &[TopicPlaceholder]) -> Vec { - placeholders - .iter() - .map(|placeholder| match placeholder { - TopicPlaceholder::DeviceType => quote!(device_type), - TopicPlaceholder::Nid => quote!(nid), + fn emit_message( + &mut self, + message_def: &Commented, + scope: &[String], + module_key: u16, + key_gen: &mut MessageKeyGen, + ) -> Result { + let mut path = scope.to_vec(); + path.push(message_def.node.name.clone()); + let members = self.registry.collect_message_members(&path); + let ident = class_name(scope, &message_def.node.name); + let struct_tokens = self.render_struct_body(&ident, &members, scope); + let scope_module_key = + annotation_u16(&message_def.annotations, "module_key").unwrap_or(module_key); + let message_key = annotation_u16(&message_def.annotations, "message_key") + .unwrap_or_else(|| key_gen.next()); + let topic = annotation_string(&message_def.annotations, "topic").ok_or( + CodegenError::MissingTopic { + message: scoped_name(scope, &message_def.node.name), + }, + )?; + let upper = uppercase_path(scope, &message_def.node.name); + let module_macro = format!("BLUEBERRY_{}_MODULE_KEY", upper); + let message_macro = format!("BLUEBERRY_{}_MESSAGE_KEY", upper); + let topic_ident = format!("BLUEBERRY_{}_TOPIC_TEMPLATE", upper); + let module_literal = format!("((uint16_t)0x{scope_module_key:04X})"); + let message_literal = format!("((uint16_t)0x{message_key:04X})"); + let topic_literal = quoted_string(&topic); + self.message_topics.push(MessageTopic { + ident: topic_ident.clone(), + literal: topic_literal.clone(), + }); + Ok(quote! { + $struct_tokens + + #define $module_macro $module_literal + #define $message_macro $message_literal + extern const char $topic_ident[]; + + $['\n'] }) - .collect() -} - -fn helpers_tokens() -> Tokens { - quote! { - static inline void blueberry_write_u8(uint8_t value, uint8_t *buf) { - buf[0] = value; - } - - static inline uint8_t blueberry_read_u8(const uint8_t *buf) { - return buf[0]; - } - - static inline void blueberry_write_i16(int16_t value, uint8_t *buf) { - buf[0] = (uint8_t)(value & 0xFF); - buf[1] = (uint8_t)((value >> 8) & 0xFF); - } - - static inline int16_t blueberry_read_i16(const uint8_t *buf) { - return (int16_t)((int16_t)buf[0] | ((int16_t)buf[1] << 8)); - } - - static inline void blueberry_write_u16(uint16_t value, uint8_t *buf) { - buf[0] = (uint8_t)(value & 0xFFu); - buf[1] = (uint8_t)((value >> 8) & 0xFFu); - } + } - static inline uint16_t blueberry_read_u16(const uint8_t *buf) { - return (uint16_t)((uint16_t)buf[0] | ((uint16_t)buf[1] << 8)); + fn emit_const(&self, const_def: &Commented, scope: &[String]) -> Tokens { + let name = class_name(scope, &const_def.node.name); + let value = Self::const_literal(&const_def.node.value); + match &const_def.node.const_type { + Type::String { .. } => quote!(static const char $name[] = $value; $['\n']), + Type::WString => quote!(static const uint16_t $name[] = $value; $['\n']), + Type::Char => quote!(static const int8_t $name = $value; $['\n']), + Type::WChar => quote!(static const uint16_t $name = $value; $['\n']), + other => { + let resolved = self.registry.resolve_type(other, scope); + let ty = self.type_tokens(&resolved, scope); + quote!(static const $ty $name = $value; $['\n']) + } } + } - static inline void blueberry_write_i32(int32_t value, uint8_t *buf) { - buf[0] = (uint8_t)(value & 0xFF); - buf[1] = (uint8_t)((value >> 8) & 0xFF); - buf[2] = (uint8_t)((value >> 16) & 0xFF); - buf[3] = (uint8_t)((value >> 24) & 0xFF); + fn emit_typedef(&self, typedef_def: &Commented, scope: &[String]) -> Tokens { + let name = class_name(scope, &typedef_def.node.name); + match &typedef_def.node.base_type { + Type::Array { + element_type, + dimensions, + } => { + let resolved = self.registry.resolve_type(element_type, scope); + let base = self.type_tokens(&resolved, scope); + let dims: Vec = dimensions.iter().map(|dim| format!("[{}]", dim)).collect(); + quote! { + typedef $base $name$(for dim in dims => $dim); + + $['\n'] + } + } + other => { + let resolved = self.registry.resolve_type(other, scope); + let ty = self.type_tokens(&resolved, scope); + quote!(typedef $ty $name; $['\n']) + } } + } - static inline int32_t blueberry_read_i32(const uint8_t *buf) { - return (int32_t)((int32_t)buf[0] - | ((int32_t)buf[1] << 8) - | ((int32_t)buf[2] << 16) - | ((int32_t)buf[3] << 24)); - } + fn render_struct_body( + &self, + ident: &str, + members: &[ResolvedMember], + scope: &[String], + ) -> Tokens { + let fields: Vec = members + .iter() + .map(|member| self.field_tokens(&member.ty, &member.name, scope)) + .collect(); + quote! { + typedef struct { + $(for field in fields => + $field + ) + } $ident; - static inline void blueberry_write_u32(uint32_t value, uint8_t *buf) { - buf[0] = (uint8_t)(value & 0xFFu); - buf[1] = (uint8_t)((value >> 8) & 0xFFu); - buf[2] = (uint8_t)((value >> 16) & 0xFFu); - buf[3] = (uint8_t)((value >> 24) & 0xFFu); + $['\n'] } + } - static inline uint32_t blueberry_read_u32(const uint8_t *buf) { - return (uint32_t)((uint32_t)buf[0] - | ((uint32_t)buf[1] << 8) - | ((uint32_t)buf[2] << 16) - | ((uint32_t)buf[3] << 24)); - } + fn field_tokens(&self, ty: &Type, name: &str, scope: &[String]) -> Tokens { + let ty_tokens = self.type_tokens(ty, scope); + quote!( $ty_tokens $name; $['\n'] ) + } - static inline void blueberry_write_i64(int64_t value, uint8_t *buf) { - for (int i = 0; i < 8; ++i) { - buf[i] = (uint8_t)((uint64_t)value >> (8 * i)); + fn type_tokens(&self, ty: &Type, scope: &[String]) -> Tokens { + match ty { + Type::Long => quote!(int32_t), + Type::Short => quote!(int16_t), + Type::UnsignedShort => quote!(uint16_t), + Type::UnsignedLong => quote!(uint32_t), + Type::LongLong => quote!(int64_t), + Type::UnsignedLongLong => quote!(uint64_t), + Type::Float => quote!(float), + Type::Double => quote!(double), + Type::LongDouble => quote!(long double), + Type::Boolean => quote!(bool), + Type::Octet => quote!(uint8_t), + Type::Char => quote!(int8_t), + Type::WChar => quote!(uint16_t), + Type::String { bound } => { + if let Some(bound) = bound { + let bound_lit = format!("{}", bound); + quote! { + struct { + size_t len; + char data[$bound_lit]; + } + } + } else { + quote! { + struct { + size_t len; + char *data; + } + } + } } - } - - static inline int64_t blueberry_read_i64(const uint8_t *buf) { - int64_t value = 0; - for (int i = 0; i < 8; ++i) { - value |= ((int64_t)buf[i]) << (8 * i); + Type::WString => quote! { + struct { + size_t len; + uint16_t *data; + } + }, + Type::Sequence { element_type, size } => { + let element = self.type_tokens(element_type, scope); + if let Some(bound) = size { + let bound_lit = format!("{}", bound); + quote! { + struct { + size_t len; + $element items[$bound_lit]; + } + } + } else { + quote! { + struct { + size_t len; + $element *items; + } + } + } } - return value; - } - - static inline void blueberry_write_u64(uint64_t value, uint8_t *buf) { - for (int i = 0; i < 8; ++i) { - buf[i] = (uint8_t)(value >> (8 * i)); + Type::ScopedName(path) => { + let ident = c_path_name(path); + quote!( $ident ) + } + Type::Array { + element_type, + dimensions, + } => { + let resolved = self.registry.resolve_type(element_type, scope); + let base = self.type_tokens(&resolved, scope); + let dims: Vec = dimensions.iter().map(|dim| format!("[{}]", dim)).collect(); + quote!( $base$(for dim in dims => $dim) ) } } + } - static inline uint64_t blueberry_read_u64(const uint8_t *buf) { - uint64_t value = 0; - for (int i = 0; i < 8; ++i) { - value |= ((uint64_t)buf[i]) << (8 * i); + fn const_literal(value: &ConstValue) -> Tokens { + match value { + ConstValue::Integer(lit) => quote!($(lit.value)), + ConstValue::Float(f) => quote!($(format!("{}", f))), + ConstValue::Fixed(fixed) => { + let mut digits = fixed.digits.clone(); + if fixed.scale > 0 { + let point = digits.len().saturating_sub(fixed.scale as usize); + digits.insert(point, '.'); + } + if fixed.negative { + digits.insert(0, '-'); + } + quote!( $(digits) ) + } + ConstValue::Binary(binary) => { + let literal = binary.to_i64(); + quote!( $literal ) + } + ConstValue::String(text) => quote!( $(quoted_string(text)) ), + ConstValue::Boolean(true) => quote!(true), + ConstValue::Boolean(false) => quote!(false), + ConstValue::Char(ch) => { + let escaped = ch.escape_default().to_string(); + quote!( $(format!("'{}'", escaped)) ) + } + ConstValue::ScopedName(path) => quote!( $(c_path_name(path)) ), + ConstValue::UnaryOp { op, expr } => { + let inner = Self::const_literal(expr); + let op_token = match op { + blueberry_ast::UnaryOperator::Plus => quote!(+), + blueberry_ast::UnaryOperator::Minus => quote!(-), + }; + quote!(( $op_token $inner )) + } + ConstValue::BinaryOp { op, left, right } => { + let lhs = Self::const_literal(left); + let rhs = Self::const_literal(right); + let op_token = match op { + blueberry_ast::BinaryOperator::Add => quote!(+), + blueberry_ast::BinaryOperator::Subtract => quote!(-), + blueberry_ast::BinaryOperator::Multiply => quote!(*), + blueberry_ast::BinaryOperator::Divide => quote!(/), + }; + quote!(( $lhs $op_token $rhs )) } - return value; } + } +} - static inline void blueberry_write_f32(float value, uint8_t *buf) { - uint32_t as_int; - memcpy(&as_int, &value, sizeof(float)); - blueberry_write_u32(as_int, buf); - } +#[derive(Default)] +struct MessageKeyGen { + next: u16, +} - static inline float blueberry_read_f32(const uint8_t *buf) { - uint32_t as_int = blueberry_read_u32(buf); - float value; - memcpy(&value, &as_int, sizeof(float)); - return value; +impl MessageKeyGen { + fn next(&mut self) -> u16 { + self.next = self.next.wrapping_add(1); + if self.next == 0 { + self.next = 1; } + self.next + } +} - static inline void blueberry_write_f64(double value, uint8_t *buf) { - uint64_t as_int; - memcpy(&as_int, &value, sizeof(double)); - blueberry_write_u64(as_int, buf); - } +fn c_path_name(path: &[String]) -> String { + path.join("_") +} - static inline double blueberry_read_f64(const uint8_t *buf) { - uint64_t as_int = blueberry_read_u64(buf); - double value; - memcpy(&value, &as_int, sizeof(value)); - return value; - } +fn scoped_name(scope: &[String], name: &str) -> String { + if scope.is_empty() { + name.to_string() + } else { + format!("{}::{}", scope.join("::"), name) + } +} - static inline void blueberry_write_header(const blueberry_message_header_t *header, uint8_t *frame) { - blueberry_write_u16(header->payload_words, frame + 0); - blueberry_write_u16(header->flags, frame + 2); - blueberry_write_u16(header->module_key, frame + 4); - blueberry_write_u16(header->message_key, frame + 6); - } +fn annotation_string(annotations: &[Annotation], name: &str) -> Option { + annotation_value(annotations, name).and_then(|value| match value { + ConstValue::String(value) => Some(value.clone()), + _ => None, + }) +} - static inline int blueberry_read_header(const uint8_t *frame, size_t frame_len, blueberry_message_header_t *out) { - if (frame_len < BLUEBERRY_MESSAGE_HEADER_SIZE) { - return -1; - } - out->payload_words = blueberry_read_u16(frame + 0); - out->flags = blueberry_read_u16(frame + 2); - out->module_key = blueberry_read_u16(frame + 4); - out->message_key = blueberry_read_u16(frame + 6); - return 0; +fn annotation_u16(annotations: &[Annotation], name: &str) -> Option { + annotation_value(annotations, name).and_then(|value| match value { + ConstValue::Integer(lit) if (0..=u16::MAX as i64).contains(&lit.value) => { + Some(lit.value as u16) } - } + _ => None, + }) +} + +fn annotation_value<'a>(annotations: &'a [Annotation], name: &str) -> Option<&'a ConstValue> { + annotations + .iter() + .find(|annotation| annotation_name_matches(annotation, name)) + .and_then(|annotation| { + annotation + .params + .iter() + .map(|param| match param { + AnnotationParam::Named { name, value } + if name.eq_ignore_ascii_case("value") => + { + value + } + AnnotationParam::Positional(value) => value, + AnnotationParam::Named { value, .. } => value, + }) + .next() + }) +} + +fn annotation_name_matches(annotation: &Annotation, expected: &str) -> bool { + annotation + .name + .last() + .map(|segment| segment.eq_ignore_ascii_case(expected)) + .unwrap_or(false) } From b227102d9bca1a110bb3168e73d05104df24d935 Mon Sep 17 00:00:00 2001 From: Joao Mario Lago Date: Wed, 3 Dec 2025 14:20:26 -0300 Subject: [PATCH 2/2] test:run_test.sh: Fix C tests to use new runtime --- test/run_tests.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/run_tests.sh b/test/run_tests.sh index 68f81f2..0f2a25d 100755 --- a/test/run_tests.sh +++ b/test/run_tests.sh @@ -8,7 +8,8 @@ IDL_PATH="${1:-${REPO_ROOT}/crates/parser/tests/fixtures/message_basic.idl}" "${SCRIPT_DIR}/generate.sh" "${IDL_PATH}" echo "Running C validation" -gcc "${REPO_ROOT}/test/c/messages.h" +gcc -c -o "${REPO_ROOT}/test/c/blueberry_runtime.o" "${REPO_ROOT}/test/c/blueberry_runtime.c" +gcc -c -o "${REPO_ROOT}/test/c/blueberry_messages.o" "${REPO_ROOT}/test/c/blueberry_messages.c" echo "Running C++ validation" gcc -std=c++20 "${REPO_ROOT}/test/cpp/messages.hpp"