diff --git a/CMakeLists.txt b/CMakeLists.txt index b2fed34d8..e9b84ecb4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,75 +18,8 @@ if (CONFIG_ARDUINO_API) zephyr_include_directories(${variant_dir}) if (CONFIG_USE_ARDUINO_API_RUST_IMPLEMENTATION) - # quote from https://github.com/zephyrproject-rtos/zephyr-lang-rust/blob/main/CMakeLists.txt - function (rust_target_arch RUST_TARGET) - # Map Zephyr targets to LLVM targets. - if(CONFIG_CPU_CORTEX_M) - if(CONFIG_CPU_CORTEX_M0 OR CONFIG_CPU_CORTEX_M0PLUS OR CONFIG_CPU_CORTEX_M1) - set(${RUST_TARGET} "thumbv6m-none-eabi" PARENT_SCOPE) - elseif(CONFIG_CPU_CORTEX_M3) - set(${RUST_TARGET} "thumbv7m-none-eabi" PARENT_SCOPE) - elseif(CONFIG_CPU_CORTEX_M4 OR CONFIG_CPU_CORTEX_M7) - if(CONFIG_FP_HARDABI OR FORCE_FP_HARDABI) - set(${RUST_TARGET} "thumbv7em-none-eabihf" PARENT_SCOPE) - else() - set(${RUST_TARGET} "thumbv7em-none-eabi" PARENT_SCOPE) - endif() - elseif(CONFIG_CPU_CORTEX_M23) - set(${RUST_TARGET} "thumbv8m.base-none-eabi" PARENT_SCOPE) - elseif(CONFIG_CPU_CORTEX_M33 OR CONFIG_CPU_CORTEX_M55) - # Not a typo, Zephyr, uses ARMV7_M_ARMV8_M_FP to select the FP even on v8m. - if(CONFIG_FP_HARDABI OR FORCE_FP_HARDABI) - set(${RUST_TARGET} "thumbv8m.main-none-eabihf" PARENT_SCOPE) - else() - set(${RUST_TARGET} "thumbv8m.main-none-eabi" PARENT_SCOPE) - endif() - - # Todo: The M55 is thumbv8.1m.main-none-eabi, which can be added when Rust - # gain support for this target. - else() - message(FATAL_ERROR "Unknown Cortex-M target.") - endif() - elseif(CONFIG_RISCV) - if(CONFIG_RISCV_ISA_RV64I) - # TODO: Should fail if the extensions don't match. - set(${RUST_TARGET} "riscv64imac-unknown-none-elf" PARENT_SCOPE) - elseif(CONFIG_RISCV_ISA_RV32I) - # TODO: We have multiple choices, try to pick the best. - set(${RUST_TARGET} "riscv32i-unknown-none-elf" PARENT_SCOPE) - else() - message(FATAL_ERROR "Rust: Unsupported riscv ISA") - endif() - elseif(CONFIG_ARCH_POSIX AND CONFIG_64BIT AND (${CMAKE_HOST_SYSTEM_PROCESSOR} MATCHES "x86_64")) - set(${RUST_TARGET} "x86_64-unknown-none" PARENT_SCOPE) - elseif(CONFIG_ARCH_POSIX AND CONFIG_64BIT AND (${CMAKE_HOST_SYSTEM_PROCESSOR} MATCHES "aarch64")) - set(${RUST_TARGET} "aarch64-unknown-none" PARENT_SCOPE) - else() - message(FATAL_ERROR "Rust: Add support for other target") - endif() - endfunction() - - rust_target_arch(RUST_TARGET_TRIPLE) - - set(RUST_CRATE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/rust) - set(RUST_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/rust) - set(RUST_LIB ${RUST_OUT_DIR}/target/${RUST_TARGET_TRIPLE}/release/libarduinocore_api_rust.a) - - add_custom_command( - OUTPUT ${RUST_LIB} - COMMAND ${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${RUST_OUT_DIR}/target - cargo build --manifest-path ${RUST_CRATE_DIR}/Cargo.toml - --target ${RUST_TARGET_TRIPLE} --release - WORKING_DIRECTORY ${RUST_CRATE_DIR} - COMMENT "Building Rust staticlib for ${RUST_TARGET}" - VERBATIM - ) - - add_custom_target(arduinocore_api_rust_build ALL DEPENDS ${RUST_LIB}) - add_library(arduinocore_api_rust STATIC IMPORTED GLOBAL) - set_target_properties(arduinocore_api_rust PROPERTIES IMPORTED_LOCATION ${RUST_LIB}) - zephyr_link_libraries(arduinocore_api_rust) - zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/zephyr/blobs/ArduinoCore-API/) + zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/alt_core_api) + add_subdirectory(alt_core_api) else() zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/zephyr/blobs/ArduinoCore-API/) zephyr_sources(${CMAKE_CURRENT_SOURCE_DIR}/zephyr/blobs/ArduinoCore-API/api/CanMsg.cpp) diff --git a/Kconfig b/Kconfig index 4d4215905..9ea63b69b 100644 --- a/Kconfig +++ b/Kconfig @@ -20,6 +20,7 @@ if ARDUINO_API config USE_ARDUINO_API_RUST_IMPLEMENTATION bool "Use Rust implementation API core" select RUST + select NANOPB config QEMU_ICOUNT bool "QEMU icount mode" diff --git a/README.md b/README.md index b703227b8..05ceb913a 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ The **Arduino Core API** module for zephyr leverages the power of Zephyr under a * [Using external Arduino Libraries](/documentation/arduino_libs.md) * [Adding custom boards/ variants](/documentation/variants.md) +* [Arduino IDL code generation](/documentation/idl_codegen.md) ## Adding Arduino Core API to Zephyr diff --git a/alt_core_api/CMakeLists.txt b/alt_core_api/CMakeLists.txt new file mode 100644 index 000000000..d9f8a28a5 --- /dev/null +++ b/alt_core_api/CMakeLists.txt @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: Apache-2.0 + +function(rust_target_arch RUST_TARGET) + # Map Zephyr targets to LLVM targets. + if(CONFIG_CPU_CORTEX_M) + if(CONFIG_CPU_CORTEX_M0 OR CONFIG_CPU_CORTEX_M0PLUS OR CONFIG_CPU_CORTEX_M1) + set(${RUST_TARGET} "thumbv6m-none-eabi" PARENT_SCOPE) + elseif(CONFIG_CPU_CORTEX_M3) + set(${RUST_TARGET} "thumbv7m-none-eabi" PARENT_SCOPE) + elseif(CONFIG_CPU_CORTEX_M4 OR CONFIG_CPU_CORTEX_M7) + if(CONFIG_FP_HARDABI OR FORCE_FP_HARDABI) + set(${RUST_TARGET} "thumbv7em-none-eabihf" PARENT_SCOPE) + else() + set(${RUST_TARGET} "thumbv7em-none-eabi" PARENT_SCOPE) + endif() + elseif(CONFIG_CPU_CORTEX_M23) + set(${RUST_TARGET} "thumbv8m.base-none-eabi" PARENT_SCOPE) + elseif(CONFIG_CPU_CORTEX_M33 OR CONFIG_CPU_CORTEX_M55) + if(CONFIG_FP_HARDABI OR FORCE_FP_HARDABI) + set(${RUST_TARGET} "thumbv8m.main-none-eabihf" PARENT_SCOPE) + else() + set(${RUST_TARGET} "thumbv8m.main-none-eabi" PARENT_SCOPE) + endif() + else() + message(FATAL_ERROR "Unknown Cortex-M target.") + endif() + elseif(CONFIG_RISCV) + if(CONFIG_RISCV_ISA_RV64I) + set(${RUST_TARGET} "riscv64imac-unknown-none-elf" PARENT_SCOPE) + elseif(CONFIG_RISCV_ISA_RV32I) + set(${RUST_TARGET} "riscv32i-unknown-none-elf" PARENT_SCOPE) + else() + message(FATAL_ERROR "Rust: Unsupported riscv ISA") + endif() + elseif(CONFIG_ARCH_POSIX AND CONFIG_64BIT AND (${CMAKE_HOST_SYSTEM_PROCESSOR} MATCHES "x86_64")) + set(${RUST_TARGET} "x86_64-unknown-none" PARENT_SCOPE) + elseif(CONFIG_ARCH_POSIX AND CONFIG_64BIT AND (${CMAKE_HOST_SYSTEM_PROCESSOR} MATCHES "aarch64")) + set(${RUST_TARGET} "aarch64-unknown-none" PARENT_SCOPE) + else() + message(FATAL_ERROR "Rust: Add support for other target") + endif() +endfunction() + +rust_target_arch(RUST_TARGET_TRIPLE) + +if (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/rust/Cargo.toml) + set(RUST_CRATE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/rust) +else() + set(RUST_CRATE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../rust) +endif() +set(RUST_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/rust) +set(RUST_LIB ${RUST_OUT_DIR}/target/${RUST_TARGET_TRIPLE}/release/libarduinocore_api_rust.a) +set(RUST_SRCS + ${RUST_CRATE_DIR}/src/lib.rs + ${RUST_CRATE_DIR}/src/common.rs +) + +add_custom_command( + OUTPUT ${RUST_LIB} + COMMAND ${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${RUST_OUT_DIR}/target + cargo build --manifest-path ${RUST_CRATE_DIR}/Cargo.toml + --target ${RUST_TARGET_TRIPLE} --release + WORKING_DIRECTORY ${RUST_CRATE_DIR} + DEPENDS ${RUST_CRATE_DIR}/Cargo.toml ${RUST_SRCS} + COMMENT "Building Rust staticlib for ${RUST_TARGET_TRIPLE}" + VERBATIM +) + +add_custom_target(arduinocore_api_rust_build ALL DEPENDS ${RUST_LIB}) +add_library(arduinocore_api_rust STATIC IMPORTED GLOBAL) +set_target_properties(arduinocore_api_rust PROPERTIES IMPORTED_LOCATION ${RUST_LIB}) +zephyr_link_libraries(arduinocore_api_rust) + +add_subdirectory(idl) +add_subdirectory(api) + +if(TARGET arduinocore_idl_generate_interfaces) + add_dependencies(app arduinocore_idl_generate_interfaces) +endif() diff --git a/alt_core_api/api/ArduinoAPI.h b/alt_core_api/api/ArduinoAPI.h new file mode 100644 index 000000000..e81e40066 --- /dev/null +++ b/alt_core_api/api/ArduinoAPI.h @@ -0,0 +1,139 @@ +#pragma once + +#ifdef __cplusplus +#include +#include +#else +#include +#include +#endif +#include + +#include +#include + +#include "common_types.h" + +//#ifdef __cplusplus +//extern "C" { +//#endif + +typedef gpio_port_pins_t pin_size_t; + +typedef void (*voidFuncPtr)(void); +typedef void (*voidFuncPtrParam)(void*); + +void setup(void); +void loop(void); + +void delay(unsigned long); + +namespace arduino { +} + +using namespace arduino; + +template constexpr const T &max(const T &a, const T &b) { + return (a < b) ? b : a; +} + +template constexpr const T &min(const T &a, const T &b) { + return (a < b) ? a : b; +} + +#if 0 +void yield(void); + +void init(void); +void initVariant(void); + +int main() __attribute__((weak)); + +void pinMode(pin_size_t pinNumber, PinMode pinMode); +void digitalWrite(pin_size_t pinNumber, PinStatus status); +PinStatus digitalRead(pin_size_t pinNumber); +int analogRead(pin_size_t pinNumber); +void analogReference(uint8_t mode); +void analogWrite(pin_size_t pinNumber, int value); + +unsigned long millis(void); +unsigned long micros(void); +void delay(unsigned long); +void delayMicroseconds(unsigned int us); +unsigned long pulseIn(pin_size_t pin, uint8_t state, unsigned long timeout); +unsigned long pulseInLong(pin_size_t pin, uint8_t state, unsigned long timeout); + +void shiftOut(pin_size_t dataPin, pin_size_t clockPin, BitOrder bitOrder, uint8_t val); +uint8_t shiftIn(pin_size_t dataPin, pin_size_t clockPin, BitOrder bitOrder); + +void attachInterrupt(pin_size_t interruptNumber, voidFuncPtr callback, PinStatus mode); +void attachInterruptParam(pin_size_t interruptNumber, voidFuncPtrParam callback, PinStatus mode, void* param); +void detachInterrupt(pin_size_t interruptNumber); + +void setup(void); +void loop(void); + +#ifdef __cplusplus +} // extern "C" +#endif + +unsigned long pulseIn(uint8_t pin, uint8_t state, unsigned long timeout = 1000000L); +unsigned long pulseInLong(uint8_t pin, uint8_t state, unsigned long timeout = 1000000L); + +void tone(uint8_t _pin, unsigned int frequency, unsigned long duration = 0); +void noTone(uint8_t _pin); + +// WMath prototypes +long random(long); +long random(long, long); +void randomSeed(unsigned long); + + +extern "C" { + int32_t map_i32(int32_t x, int32_t in_min, int32_t in_max, int32_t out_min, int32_t out_max); + uint16_t makeWord_w(uint16_t w); + uint16_t makeWord_hl(uint8_t h, uint8_t l); +} + +inline long map(long x, long in_min, long in_max, long out_min, long out_max) +{ + return map_i32(x, in_min, in_max, out_min, out_max); +} + +inline uint16_t makeWord(uint16_t w) { + return makeWord_w(w); +} + +inline uint16_t makeWord(uint8_t h, uint8_t l) { + return makeWord_hl(h, l); +} + +#define word(...) makeWord(__VA_ARGS__) + +#ifdef __cplusplus + template + auto min(const T& a, const L& b) -> decltype((b < a) ? b : a) + { + return (b < a) ? b : a; + } + + template + auto max(const T& a, const L& b) -> decltype((b < a) ? b : a) + { + return (a < b) ? b : a; + } +#else +#ifndef min +#define min(a,b) \ + ({ __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a < _b ? _a : _b; }) +#endif +#ifndef max +#define max(a,b) \ + ({ __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a > _b ? _a : _b; }) +#endif +#endif +#endif diff --git a/alt_core_api/api/CMakeLists.txt b/alt_core_api/api/CMakeLists.txt new file mode 100644 index 000000000..503edbc76 --- /dev/null +++ b/alt_core_api/api/CMakeLists.txt @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Apache-2.0 + +zephyr_sources(zephyrPrint.cpp) diff --git a/alt_core_api/api/Common.h b/alt_core_api/api/Common.h new file mode 100644 index 000000000..e6bf58f68 --- /dev/null +++ b/alt_core_api/api/Common.h @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2026 TOKITA Hiroshi + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* stub file for compat */ diff --git a/alt_core_api/api/HardwareSerial.h b/alt_core_api/api/HardwareSerial.h new file mode 100644 index 000000000..e63c4b722 --- /dev/null +++ b/alt_core_api/api/HardwareSerial.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 TOKITA Hiroshi + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "Stream.h" +#include + +namespace arduino { + +enum SerialConfig { + SERIAL_5E1 = (SERIAL_DATA_5 | SERIAL_PARITY_EVEN | SERIAL_STOP_BIT_1), + SERIAL_5E2 = (SERIAL_DATA_5 | SERIAL_PARITY_EVEN | SERIAL_STOP_BIT_2), + SERIAL_5O1 = (SERIAL_DATA_5 | SERIAL_PARITY_ODD | SERIAL_STOP_BIT_1), + SERIAL_5O2 = (SERIAL_DATA_5 | SERIAL_PARITY_ODD | SERIAL_STOP_BIT_2), + SERIAL_5N1 = (SERIAL_DATA_5 | SERIAL_PARITY_NONE | SERIAL_STOP_BIT_1), + SERIAL_5N2 = (SERIAL_DATA_5 | SERIAL_PARITY_NONE | SERIAL_STOP_BIT_2), + SERIAL_5M1 = (SERIAL_DATA_5 | SERIAL_PARITY_MARK | SERIAL_STOP_BIT_1), + SERIAL_5M2 = (SERIAL_DATA_5 | SERIAL_PARITY_MARK | SERIAL_STOP_BIT_2), + SERIAL_5S1 = (SERIAL_DATA_5 | SERIAL_PARITY_SPACE | SERIAL_STOP_BIT_1), + SERIAL_5S2 = (SERIAL_DATA_5 | SERIAL_PARITY_SPACE | SERIAL_STOP_BIT_2), + SERIAL_6E1 = (SERIAL_DATA_6 | SERIAL_PARITY_EVEN | SERIAL_STOP_BIT_1), + SERIAL_6E2 = (SERIAL_DATA_6 | SERIAL_PARITY_EVEN | SERIAL_STOP_BIT_2), + SERIAL_6O1 = (SERIAL_DATA_6 | SERIAL_PARITY_ODD | SERIAL_STOP_BIT_1), + SERIAL_6O2 = (SERIAL_DATA_6 | SERIAL_PARITY_ODD | SERIAL_STOP_BIT_2), + SERIAL_6N1 = (SERIAL_DATA_6 | SERIAL_PARITY_NONE | SERIAL_STOP_BIT_1), + SERIAL_6N2 = (SERIAL_DATA_6 | SERIAL_PARITY_NONE | SERIAL_STOP_BIT_2), + SERIAL_6M1 = (SERIAL_DATA_6 | SERIAL_PARITY_MARK | SERIAL_STOP_BIT_1), + SERIAL_6M2 = (SERIAL_DATA_6 | SERIAL_PARITY_MARK | SERIAL_STOP_BIT_2), + SERIAL_6S1 = (SERIAL_DATA_6 | SERIAL_PARITY_SPACE | SERIAL_STOP_BIT_1), + SERIAL_6S2 = (SERIAL_DATA_6 | SERIAL_PARITY_SPACE | SERIAL_STOP_BIT_2), + SERIAL_7E1 = (SERIAL_DATA_7 | SERIAL_PARITY_EVEN | SERIAL_STOP_BIT_1), + SERIAL_7E2 = (SERIAL_DATA_7 | SERIAL_PARITY_EVEN | SERIAL_STOP_BIT_2), + SERIAL_7O1 = (SERIAL_DATA_7 | SERIAL_PARITY_ODD | SERIAL_STOP_BIT_1), + SERIAL_7O2 = (SERIAL_DATA_7 | SERIAL_PARITY_ODD | SERIAL_STOP_BIT_2), + SERIAL_7N1 = (SERIAL_DATA_7 | SERIAL_PARITY_NONE | SERIAL_STOP_BIT_1), + SERIAL_7N2 = (SERIAL_DATA_7 | SERIAL_PARITY_NONE | SERIAL_STOP_BIT_2), + SERIAL_7M1 = (SERIAL_DATA_7 | SERIAL_PARITY_MARK | SERIAL_STOP_BIT_1), + SERIAL_7M2 = (SERIAL_DATA_7 | SERIAL_PARITY_MARK | SERIAL_STOP_BIT_2), + SERIAL_7S1 = (SERIAL_DATA_7 | SERIAL_PARITY_SPACE | SERIAL_STOP_BIT_1), + SERIAL_7S2 = (SERIAL_DATA_7 | SERIAL_PARITY_SPACE | SERIAL_STOP_BIT_2), + SERIAL_8E1 = (SERIAL_DATA_8 | SERIAL_PARITY_EVEN | SERIAL_STOP_BIT_1), + SERIAL_8E2 = (SERIAL_DATA_8 | SERIAL_PARITY_EVEN | SERIAL_STOP_BIT_2), + SERIAL_8O1 = (SERIAL_DATA_8 | SERIAL_PARITY_ODD | SERIAL_STOP_BIT_1), + SERIAL_8O2 = (SERIAL_DATA_8 | SERIAL_PARITY_ODD | SERIAL_STOP_BIT_2), + SERIAL_8N1 = (SERIAL_DATA_8 | SERIAL_PARITY_NONE | SERIAL_STOP_BIT_1), + SERIAL_8N2 = (SERIAL_DATA_8 | SERIAL_PARITY_NONE | SERIAL_STOP_BIT_2), + SERIAL_8M1 = (SERIAL_DATA_8 | SERIAL_PARITY_MARK | SERIAL_STOP_BIT_1), + SERIAL_8M2 = (SERIAL_DATA_8 | SERIAL_PARITY_MARK | SERIAL_STOP_BIT_2), + SERIAL_8S1 = (SERIAL_DATA_8 | SERIAL_PARITY_SPACE | SERIAL_STOP_BIT_1), + SERIAL_8S2 = (SERIAL_DATA_8 | SERIAL_PARITY_SPACE | SERIAL_STOP_BIT_2), +}; + +class HardwareSerial : virtual public Stream, virtual public HardwareSerialInterface { +public: + using Print::write; +}; + +extern void __weak serialEventRun(void); + +} // namespace arduino diff --git a/alt_core_api/api/Print.h b/alt_core_api/api/Print.h new file mode 100644 index 000000000..598a121df --- /dev/null +++ b/alt_core_api/api/Print.h @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2026 TOKITA Hiroshi + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +#include + +namespace arduino { + +class String; +class Printable; + +class Print : virtual public PrintInterface { + class ErrorCode { + int code; + + void setError(int err) { + code = err; + } + + int getError() { + return code; + } + + void clearError() { + setError(0); + } + + friend Print; + } errcode; + +protected: + void setWriteError(int err = 1) { + errcode.setError(err); + } + +public: + using PrintInterface::write; + + int getWriteError() { + return errcode.getError(); + } + + void clearWriteError() { + errcode.clearError(); + } + + virtual size_t write(const uint8_t *buffer, size_t size); + + size_t write(const char *buffer, size_t size) { + if (buffer != nullptr && size != 0) { + return write(reinterpret_cast(buffer), size); + } else { + return 0; + } + } + + size_t write(const char *str) { + return write(str, strlen(str)); + } + + int availableForWrite() { + return 0; + } + + void flush() { + } + + size_t print(const String &); + size_t print(const char[]); + size_t print(char); + size_t print(unsigned char, int = 10); + size_t print(int, int = 10); + size_t print(unsigned int, int = 10); + size_t print(long, int = 10); + size_t print(unsigned long, int = 10); + size_t print(long long, int = 10); + size_t print(unsigned long long, int = 10); + size_t print(double, int = 2); + size_t print(const Printable &); + + size_t println(const String &s); + size_t println(const char[]); + size_t println(char); + size_t println(unsigned char, int = 10); + size_t println(int, int = 10); + size_t println(unsigned int, int = 10); + size_t println(long, int = 10); + size_t println(unsigned long, int = 10); + size_t println(long long, int = 10); + size_t println(unsigned long long, int = 10); + size_t println(double, int = 2); + size_t println(const Printable &); + size_t println(void); +}; + +namespace zephyr { + +int cbprintf_callback(int c, void *ctx); +size_t wrap_cbprintf(void *ctx, const char *format, ...); +size_t print_number_base_any(void *ctx, unsigned long long ull, int base); +size_t print_number_base_pow2(void *ctx, unsigned long long ull, unsigned bits); + +template +size_t print_number(void *ctx, Number n, const int base, const char *decfmt) { + if (base == 0) { + return reinterpret_cast(ctx)->write(static_cast(n)); + } else if (base == 2) { + return zephyr::print_number_base_pow2(ctx, n, 1); + } else if (base == 4) { + return zephyr::print_number_base_pow2(ctx, n, 2); + } else if (base == 8) { + return zephyr::print_number_base_pow2(ctx, n, 3); + } else if (base == 10) { + return zephyr::wrap_cbprintf(ctx, decfmt, n); + } else if (base == 16) { + return zephyr::print_number_base_pow2(ctx, n, 4); + } else if (base == 32) { + return zephyr::print_number_base_pow2(ctx, n, 5); + } else { + return zephyr::print_number_base_any(ctx, n, base); + } +} + +} // namespace zephyr + +//inline size_t Print::print(const String &s) { +// return write(s.c_str(), s.length()); +//} + +inline size_t Print::print(const char str[]) { + return write(str); +} + +inline size_t Print::print(char c) { + return write(c); +} + +inline size_t Print::print(unsigned char n, int base) { + return zephyr::print_number(this, n, base, "%hhu"); +} + +inline size_t Print::print(int n, int base) { + return zephyr::print_number(this, n, base, "%d"); +} + +inline size_t Print::print(unsigned int n, int base) { + return zephyr::print_number(this, n, base, "%u"); +} + +inline size_t Print::print(long n, int base) { + return zephyr::print_number(this, n, base, "%ld"); +} + +inline size_t Print::print(unsigned long n, int base) { + return zephyr::print_number(this, n, base, "%lu"); +} + +inline size_t Print::print(long long n, int base) { + return zephyr::print_number(this, n, base, "%lld"); +} + +inline size_t Print::print(unsigned long long n, int base) { + return zephyr::print_number(this, n, base, "%llu"); +} + +inline size_t Print::print(double n, int perception) { + if (perception < 10) { + const char ch_perception = static_cast('0' + perception); + const char format[] = {'%', '.', ch_perception, 'f', '\0'}; + return zephyr::wrap_cbprintf(this, format, n); + } else { + const char ch_perception = static_cast('0' + (perception % 10)); + const char format[] = {'%', '.', '1', ch_perception, 'f', '\0'}; + return zephyr::wrap_cbprintf(this, format, n); + } +} + +//inline size_t Print::print(const Printable &printable) { +// return printable.printTo(*this); +//} + +inline size_t Print::println(const String &s) { + return print(s) + println(); +} + +inline size_t Print::println(const char str[]) { + return print(str) + println(); +} + +inline size_t Print::println(char c) { + return print(c) + println(); +} + +inline size_t Print::println(unsigned char uc, int base) { + return print(uc, base) + println(); +} + +inline size_t Print::println(int i, int base) { + return print(i, base) + println(); +} + +inline size_t Print::println(unsigned int ui, int base) { + return print(ui, base) + println(); +} + +inline size_t Print::println(long l, int base) { + return print(l, base) + println(); +} + +inline size_t Print::println(unsigned long ul, int base) { + return print(ul, base) + println(); +} + +inline size_t Print::println(long long ll, int base) { + return print(ll, base) + println(); +} + +inline size_t Print::println(unsigned long long ull, int base) { + return print(ull, base) + println(); +} + +inline size_t Print::println(double d, int perception) { + return print(d, perception) + println(); +} + +inline size_t Print::println(const Printable &printable) { + return print(printable) + println(); +} + +inline size_t Print::println(void) { + return write("\r\n", 2); +} + +/* + * This is the default implementation. + * It will be overridden by subclasses. + */ +inline size_t arduino::Print::write(const uint8_t *buffer, size_t size) { + size_t i; + + for (i = 0; i < size && write(buffer[i]); i++) { + } + + return i; +} +} // namespace arduino diff --git a/alt_core_api/api/Stream.h b/alt_core_api/api/Stream.h new file mode 100644 index 000000000..7687c0c57 --- /dev/null +++ b/alt_core_api/api/Stream.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2026 TOKITA Hiroshi + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "Print.h" +#include + +namespace arduino { + +class Stream : virtual public Print, virtual public StreamInterface { +}; + +}; // namespace arduino diff --git a/cores/arduino/zephyrPrint.cpp b/alt_core_api/api/zephyrPrint.cpp similarity index 88% rename from cores/arduino/zephyrPrint.cpp rename to alt_core_api/api/zephyrPrint.cpp index 2e07c2329..bf250532d 100644 --- a/cores/arduino/zephyrPrint.cpp +++ b/alt_core_api/api/zephyrPrint.cpp @@ -87,16 +87,3 @@ size_t print_number_base_pow2(void *ctx, unsigned long long ull, unsigned bits) } // namespace zephyr } // namespace arduino - -/* - * This is the default implementation. - * It will be overridden by subclassese. - */ -size_t arduino::Print::write(const uint8_t *buffer, size_t size) -{ - size_t i; - for (i=0; i str: + if package_name: + return f".{package_name}.{service_name}" + return f".{service_name}" + + +def _load_module_from_path(source_path: Path, module_name: str) -> types.ModuleType: + spec = importlib.util.spec_from_file_location(module_name, source_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"failed to load arduino_opts_pb2 from '{source_path}'") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +@lru_cache(maxsize=1) +def get_arduino_opts_pb2() -> types.ModuleType: + pb2_path = os.environ.get("PROTOC_GEN_ARDUINOIF_PB2") + if not pb2_path: + raise RuntimeError("PROTOC_GEN_ARDUINOIF_PB2 is not set") + + source_path = Path(pb2_path) + if not source_path.exists(): + raise RuntimeError( + f"PROTOC_GEN_ARDUINOIF_PB2 points to missing file: '{source_path}'" + ) + + return _load_module_from_path( + source_path, "_protoc_gen_arduinoif_arduino_opts_pb2" + ) diff --git a/alt_core_api/tools/protoc_gen_arduinoif/core.py b/alt_core_api/tools/protoc_gen_arduinoif/core.py new file mode 100755 index 000000000..5ea49338e --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/core.py @@ -0,0 +1,64 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import sys +from typing import Iterator, Tuple + +from google.protobuf.compiler import plugin_pb2 + +from .common import get_arduino_opts_pb2 +from .enum_renderer import EnumRenderer +from .service_renderer import ServiceRenderer +from .service_model_builder import ServiceModelBuilder +from .request_context import RequestContext + +__all__ = [ + "main", +] + + +def _iter_generated_files( + request: plugin_pb2.CodeGeneratorRequest, +) -> Iterator[Tuple[str, str]]: + context = RequestContext.build(request) + + for proto_file in request.proto_file: + if not context.is_requested_proto(proto_file.name): + continue + + if proto_file.enum_type and not proto_file.service: + yield from EnumRenderer(proto_file.name, list(proto_file.enum_type)) + + for service in proto_file.service: + service_model = ServiceModelBuilder.build( + service, + proto_file.package, + list(proto_file.enum_type), + context, + ) + yield from ServiceRenderer(service_model) + + +def main() -> int: + # Extensions must be registered before parsing request payload. + get_arduino_opts_pb2() + request = plugin_pb2.CodeGeneratorRequest() + request.ParseFromString(sys.stdin.buffer.read()) + response = plugin_pb2.CodeGeneratorResponse() + + try: + for name, content in _iter_generated_files(request): + output = response.file.add() + output.name = name + output.content = content + except Exception as error: + response.error = str(error) + + if hasattr(plugin_pb2.CodeGeneratorResponse, "FEATURE_PROTO3_OPTIONAL"): + response.supported_features = ( + plugin_pb2.CodeGeneratorResponse.FEATURE_PROTO3_OPTIONAL + ) + + sys.stdout.buffer.write(response.SerializeToString()) + return 0 diff --git a/alt_core_api/tools/protoc_gen_arduinoif/enum_renderer.py b/alt_core_api/tools/protoc_gen_arduinoif/enum_renderer.py new file mode 100644 index 000000000..5c9a0f06a --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/enum_renderer.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pathlib import PurePosixPath +from typing import Iterator, List, Tuple + +from google.protobuf.descriptor_pb2 import EnumDescriptorProto + +__all__ = [ + "EnumRenderer", +] + + +class EnumRenderer: + def __init__(self, proto_name: str, enums: List[EnumDescriptorProto]) -> None: + self._proto_name = proto_name + self._enums = enums + + def __iter__(self) -> Iterator[Tuple[str, str]]: + yield self._header_name(), self._render_header_content() + + def _header_name(self) -> str: + return f"{PurePosixPath(self._proto_name).stem}_types.h" + + def _render_header_content(self) -> str: + lines = [ + "// Generated by protoc-gen-arduinoif. DO NOT EDIT.", + "", + "#pragma once", + "", + "#include ", + "", + ] + + for enum_desc in self._enums: + lines.extend([f"typedef enum {enum_desc.name} {{"]) + lines.extend( + f" {value.name} = {value.number}," for value in enum_desc.value + ) + lines.extend([f"}} {enum_desc.name};", ""]) + + while lines[-1] == "": + lines.pop() + lines.append("") + return "\n".join(lines) diff --git a/alt_core_api/tools/protoc_gen_arduinoif/method_spec.py b/alt_core_api/tools/protoc_gen_arduinoif/method_spec.py new file mode 100644 index 000000000..40b503242 --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/method_spec.py @@ -0,0 +1,149 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Dict, List, NamedTuple, Optional, Tuple + +from google.protobuf.descriptor_pb2 import DescriptorProto, FieldDescriptorProto + +__all__ = [ + "MethodSpec", +] + +_DEFAULT_FIELD_CPP_TYPES: Dict[int, str] = { + FieldDescriptorProto.TYPE_BOOL: "bool", + FieldDescriptorProto.TYPE_INT32: "int32_t", + FieldDescriptorProto.TYPE_INT64: "int64_t", + FieldDescriptorProto.TYPE_UINT32: "uint32_t", + FieldDescriptorProto.TYPE_UINT64: "uint64_t", + FieldDescriptorProto.TYPE_SINT32: "int32_t", + FieldDescriptorProto.TYPE_SINT64: "int64_t", + FieldDescriptorProto.TYPE_FIXED32: "uint32_t", + FieldDescriptorProto.TYPE_FIXED64: "uint64_t", + FieldDescriptorProto.TYPE_SFIXED32: "int32_t", + FieldDescriptorProto.TYPE_SFIXED64: "int64_t", + FieldDescriptorProto.TYPE_FLOAT: "float", + FieldDescriptorProto.TYPE_DOUBLE: "double", + FieldDescriptorProto.TYPE_STRING: "const char *", + FieldDescriptorProto.TYPE_BYTES: "const uint8_t *", + FieldDescriptorProto.TYPE_ENUM: "int32_t", +} + + +def _has_extension(options, extension) -> bool: + try: + return options.HasExtension(extension) + except (AttributeError, KeyError): + return False + + +def _option_string(options, extension) -> str: + if not _has_extension(options, extension): + return "" + return str(options.Extensions[extension]) + + +def _option_string_list(options, extension) -> List[str]: + try: + values = options.Extensions[extension] + except (AttributeError, KeyError): + return [] + return [text for text in (str(value).strip() for value in values) if text] + + +def _option_bool(options, extension, default: bool = False) -> bool: + if not _has_extension(options, extension): + return default + return bool(options.Extensions[extension]) + + +class MethodSpec(NamedTuple): + decl: str + call_name: str + arg_names: List[str] + returns_void: bool + source_virtual: bool + emit_api: bool + emit_service: bool + visibility: str + + @classmethod + def build( + cls, + method, + *, + opts_pb2, + message_map: Dict[str, DescriptorProto], + default_types: Optional[Dict[int, str]] = None, + ) -> "MethodSpec": + default_field_types = default_types or _DEFAULT_FIELD_CPP_TYPES + method_opts = method.options + + source_virtual = _option_bool(method_opts, opts_pb2.source_virtual, True) + emit_api = _option_bool(method_opts, opts_pb2.emit_api, True) + emit_service = _option_bool(method_opts, opts_pb2.emit_service, True) + + visibility = _option_string(method_opts, opts_pb2.method_visibility).strip().lower() + if not visibility: + visibility = "public" + if visibility not in {"public", "protected", "private"}: + raise ValueError( + f"{method.name}: unsupported method_visibility '{visibility}' " + "(expected: public, protected, private)" + ) + + method_name = _option_string(method_opts, opts_pb2.cpp_name).strip() or method.name + + def resolve_field(field: FieldDescriptorProto) -> Tuple[str, str]: + field_opts = field.options + field_type = ( + _option_string(field_opts, opts_pb2.cpp_type).strip() + or default_field_types.get(field.type, "int32_t") + ) + field_name = _option_string(field_opts, opts_pb2.field_cpp_name).strip() or field.name + return field_type, field_name + + return_type = _option_string(method_opts, opts_pb2.cpp_return).strip() + if not return_type: + output_message = message_map.get(method.output_type) + if output_message is None: + return_type = "void" + elif len(output_message.field) == 1: + return_type = resolve_field(output_message.field[0])[0] + else: + return_type = "void" + + arg_names: List[str] = [] + param_decls: List[str] = [] + arg_types = _option_string_list(method_opts, opts_pb2.cpp_arg_types) + if arg_types: + arg_names = [f"arg{index}" for index in range(len(arg_types))] + param_decls = [ + f"{arg_type} {arg_name}" + for arg_type, arg_name in zip(arg_types, arg_names) + ] + else: + input_message = message_map.get(method.input_type) + if input_message is not None: + for field in sorted(input_message.field, key=lambda f: f.number): + field_type, field_name = resolve_field(field) + param_decls.append(f"{field_type} {field_name}") + arg_names.append(field_name) + + params_blob = ", ".join(param_decls) + decl = ( + f"{method_name}({params_blob})" + if method_name.startswith("operator ") + else f"{return_type} {method_name}({params_blob})" + ) + + return cls( + decl=decl, + call_name=method_name, + arg_names=arg_names, + returns_void=(return_type == "void"), + source_virtual=source_virtual, + emit_api=emit_api, + emit_service=emit_service, + visibility=visibility, + ) diff --git a/alt_core_api/tools/protoc_gen_arduinoif/request_context.py b/alt_core_api/tools/protoc_gen_arduinoif/request_context.py new file mode 100644 index 000000000..0a3dcb094 --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/request_context.py @@ -0,0 +1,73 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import PurePosixPath +from typing import Dict, List, Set + +from google.protobuf.compiler import plugin_pb2 +from google.protobuf.descriptor_pb2 import DescriptorProto + +from .common import ServiceDescriptor, full_service_name + +__all__ = [ + "RequestContext", +] + +@dataclass +class RequestContext: + message_map: Dict[str, DescriptorProto] + service_index: Dict[str, ServiceDescriptor] + lineage_cache: Dict[str, List[str]] + requested_files: Set[str] + requested_basenames: Set[str] + + @classmethod + def build(cls, request: plugin_pb2.CodeGeneratorRequest) -> "RequestContext": + message_map: Dict[str, DescriptorProto] = {} + service_index: Dict[str, ServiceDescriptor] = {} + + for proto_file in request.proto_file: + prefix = f".{proto_file.package}" if proto_file.package else "" + for message in proto_file.message_type: + cls._add_message(message_map, prefix, "", message) + for service in proto_file.service: + full_name = full_service_name(proto_file.package, service.name) + service_index[full_name] = (service, proto_file.package) + + requested_files = set(request.file_to_generate) + requested_basenames = {PurePosixPath(name).name for name in requested_files} + return cls( + message_map=message_map, + service_index=service_index, + lineage_cache={}, + requested_files=requested_files, + requested_basenames=requested_basenames, + ) + + def is_requested_proto(self, proto_name: str) -> bool: + proto_basename = PurePosixPath(proto_name).name + return ( + proto_name in self.requested_files + or proto_basename in self.requested_basenames + ) + + @staticmethod + def _add_message( + message_map: Dict[str, DescriptorProto], + package_prefix: str, + parent_name: str, + message: DescriptorProto, + ) -> None: + if parent_name: + full_name = f"{package_prefix}.{parent_name}.{message.name}" + else: + full_name = f"{package_prefix}.{message.name}" + message_map[full_name] = message + + child_parent = full_name[len(package_prefix) + 1 :] + for nested in message.nested_type: + RequestContext._add_message( + message_map, package_prefix, child_parent, nested + ) diff --git a/alt_core_api/tools/protoc_gen_arduinoif/service_model_builder.py b/alt_core_api/tools/protoc_gen_arduinoif/service_model_builder.py new file mode 100644 index 000000000..044bd32a4 --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/service_model_builder.py @@ -0,0 +1,383 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pathlib import PurePosixPath +from typing import Dict, List, NamedTuple, Optional + +from google.protobuf.descriptor_pb2 import EnumDescriptorProto + +from .common import ( + MethodSpec, + ServiceModel, + full_service_name, + get_arduino_opts_pb2, +) +from .request_context import RequestContext + +__all__ = [ + "ServiceModelBuilder", +] + + +class _OptionsView: + def __init__(self, options) -> None: + self._options = options + + def has(self, extension) -> bool: + try: + return self._options.HasExtension(extension) + except (AttributeError, KeyError): + return False + + def string(self, extension) -> str: + if not self.has(extension): + return "" + return str(self._options.Extensions[extension]) + + def string_list(self, extension) -> List[str]: + try: + values = self._options.Extensions[extension] + except (AttributeError, KeyError): + return [] + return [text for text in (str(value).strip() for value in values) if text] + + def bool(self, extension, default: bool = False) -> bool: + if not self.has(extension): + return default + return bool(self._options.Extensions[extension]) + + +class _PlannedMethod(NamedTuple): + spec: MethodSpec + in_ifc: bool + in_api: bool + in_service: bool + in_service_impl: bool + + @classmethod + def build( + cls, + spec: MethodSpec, + own_virtual_decls: set[str], + service_impl_callable: set[str], + ) -> "_PlannedMethod": + in_service = spec.emit_service + return cls( + spec=spec, + in_ifc=spec.decl in own_virtual_decls, + in_api=spec.emit_api and spec.source_virtual, + in_service=in_service, + in_service_impl=in_service and spec.call_name in service_impl_callable, + ) + +class _ResolvedServiceOptions(NamedTuple): + ifc_name: str + api_name: str + service_name: str + service_impl_name: str + api_member_name: str + generate_api: bool + generate_service: bool + generate_service_impl: bool + + +class ServiceModelBuilder: + def __init__( + self, + service, + package_name: str, + proto_enums: List[EnumDescriptorProto], + context: RequestContext, + ) -> None: + self._service = service + self._package_name = package_name + self._proto_enums = proto_enums + self._context = context + self._opts_pb2 = get_arduino_opts_pb2() + self._service_full_name = full_service_name(package_name, service.name) + + @classmethod + def build( + cls, + service, + package_name: str, + proto_enums: List[EnumDescriptorProto], + context: RequestContext, + ) -> ServiceModel: + return cls(service, package_name, proto_enums, context)._build() + + def _build(self) -> ServiceModel: + service_opts = _OptionsView(self._service.options) + + ifc_header = ( + service_opts.string(self._opts_pb2.ifc_header_name).strip() + or f"{self._snake_case(self._service.name)}_interface.hpp" + ) + if ifc_header.endswith("_interface.hpp"): + stem = ifc_header[: -len("_interface.hpp")] + else: + stem = PurePosixPath(ifc_header).stem + + options = _ResolvedServiceOptions( + ifc_name=service_opts.string(self._opts_pb2.ifc_class_name).strip() + or f"{self._service.name}Interface", + api_name=service_opts.string(self._opts_pb2.api_class_name).strip() + or f"{self._service.name}Api", + service_name=service_opts.string(self._opts_pb2.service_class_name).strip() + or f"{self._service.name}Service", + service_impl_name=service_opts.string( + self._opts_pb2.service_impl_class_name + ).strip() + or f"{self._service.name}ServiceImpl", + api_member_name=service_opts.string(self._opts_pb2.api_member_name).strip() + or "api_", + generate_api=service_opts.bool(self._opts_pb2.generate_api_class, False), + generate_service=service_opts.bool( + self._opts_pb2.generate_service_class, False + ), + generate_service_impl=service_opts.bool( + self._opts_pb2.generate_service_impl_class, False + ), + ) + self._validate_generation_flags(options) + + lineage = self._collect_service_lineage(self._service_full_name) + own_by_decl: Dict[str, MethodSpec] = {} + for method in self._service.method: + spec = MethodSpec.build( + method, + opts_pb2=self._opts_pb2, + message_map=self._context.message_map, + ) + own_by_decl[spec.decl] = spec + own_virtual_decls = { + spec.decl for spec in own_by_decl.values() if spec.source_virtual + } + lineage_specs = self._collect_lineage_methods(lineage) + api_callable = { + spec.call_name + for spec in lineage_specs + if spec.emit_api and spec.source_virtual + } + service_callable = { + spec.call_name for spec in lineage_specs if spec.emit_service + } + + self._validate_api_methods(options, lineage_specs) + self._validate_service_impl_api_delegate(options, lineage_specs, api_callable) + + service_impl_callable = ( + api_callable + if (options.generate_service_impl and options.generate_api) + else service_callable + ) + methods = [ + _PlannedMethod.build(spec, own_virtual_decls, service_impl_callable) + for spec in lineage_specs + ] + + include_list = self._uniq(self._collect_lineage_includes(lineage)) + + base_ifc_names: List[str] = [] + base_ifc_headers: List[str] = [] + for ancestor_full_name in lineage[:-1]: + ancestor_service, _ = self._context.service_index[ancestor_full_name] + ancestor_opts = _OptionsView(ancestor_service.options) + base_ifc_names.append( + ancestor_opts.string(self._opts_pb2.ifc_class_name).strip() + or f"{ancestor_service.name}Interface" + ) + base_ifc_headers.append( + ancestor_opts.string(self._opts_pb2.ifc_header_name).strip() + or f"{self._snake_case(ancestor_service.name)}_interface.hpp" + ) + base_ifc_names = self._uniq(base_ifc_names) + base_ifc_headers = self._uniq(base_ifc_headers) + + api_header = f"{stem}_api.hpp" + service_header = f"{stem}_service.hpp" + service_impl_header = f"{stem}_service_impl.hpp" + + api_includes = [ifc_header, *include_list] + + service_includes: List[str] = [] + if options.generate_service: + service_includes = self._uniq([*base_ifc_headers, *include_list]) + if self._proto_enums: + service_includes = self._uniq([ifc_header, *service_includes]) + + service_impl_includes: List[str] = [] + if options.generate_service_impl: + service_impl_includes = [service_header, *include_list] + if options.generate_api: + service_impl_includes.insert(1, api_header) + + namespace_name = "::".join( + part for part in self._package_name.split(".") if part + ) + + return ServiceModel( + include_list=include_list, + api_includes=api_includes, + service_includes=service_includes, + service_impl_includes=service_impl_includes, + namespace_name=namespace_name, + proto_enums=self._proto_enums, + ifc_name=options.ifc_name, + api_name=options.api_name, + service_name=options.service_name, + service_impl_name=options.service_impl_name, + api_member_name=options.api_member_name, + service_base_ifc_class_names=base_ifc_names, + ifc_header=ifc_header, + api_header=api_header, + service_header=service_header, + service_impl_header=service_impl_header, + methods=methods, + generate_api=options.generate_api, + generate_service=options.generate_service, + generate_service_impl=options.generate_service_impl, + ) + + def _collect_lineage_methods(self, lineage: List[str]) -> List[MethodSpec]: + by_decl: Dict[str, MethodSpec] = {} + for service_full_name in lineage: + service, _ = self._context.service_index[service_full_name] + for method in service.method: + spec = MethodSpec.build( + method, + opts_pb2=self._opts_pb2, + message_map=self._context.message_map, + ) + by_decl[spec.decl] = spec + return list(by_decl.values()) + + def _validate_generation_flags( + self, + options: _ResolvedServiceOptions, + ) -> None: + if options.generate_service_impl and not options.generate_service: + raise ValueError( + f"{self._service.name}: generate_service_impl_class=true requires generate_service_class=true" + ) + + def _validate_api_methods( + self, + options: _ResolvedServiceOptions, + lineage_method_specs, + ) -> None: + if not options.generate_api: + return + invalid_api_methods = [ + spec.decl + for spec in lineage_method_specs + if spec.emit_api and not spec.source_virtual + ] + if invalid_api_methods: + joined = ", ".join(invalid_api_methods) + raise ValueError( + f"{self._service.name}: generate_api_class=true cannot emit non-virtual-source methods in API ({joined})" + ) + + def _validate_service_impl_api_delegate( + self, + options: _ResolvedServiceOptions, + lineage_method_specs, + api_callable, + ) -> None: + if not (options.generate_service_impl and options.generate_api): + return + missing_api = [ + spec.decl + for spec in lineage_method_specs + if spec.emit_service and spec.call_name not in api_callable + ] + if missing_api: + joined = ", ".join(missing_api) + raise ValueError( + f"{self._service.name}: service impl delegates API, but API does not expose ({joined})" + ) + + def _collect_service_lineage( + self, + service_full_name: str, + visiting: Optional[List[str]] = None, + ) -> List[str]: + if service_full_name in self._context.lineage_cache: + return self._context.lineage_cache[service_full_name] + if visiting is None: + visiting = [] + if service_full_name in visiting: + cycle = " -> ".join([*visiting, service_full_name]) + raise ValueError(f"cyclic service inheritance detected: {cycle}") + + entry = self._context.service_index.get(service_full_name) + if entry is None: + raise ValueError(f"service '{service_full_name}' not found") + + service, package_name = entry + inherited: List[str] = [] + for base_ref in _OptionsView(service.options).string_list( + self._opts_pb2.base_services + ): + base_full_name = self._resolve_service_reference(base_ref, package_name) + inherited.extend( + self._collect_service_lineage( + base_full_name, + [*visiting, service_full_name], + ) + ) + + lineage = self._uniq([*inherited, service_full_name]) + self._context.lineage_cache[service_full_name] = lineage + return lineage + + def _resolve_service_reference( + self, + reference: str, + current_package: str, + ) -> str: + if reference.startswith("."): + candidate = reference + elif "." in reference: + candidate = f".{reference}" + elif current_package: + candidate = f".{current_package}.{reference}" + else: + candidate = f".{reference}" + + if candidate not in self._context.service_index: + raise ValueError( + f"service '{candidate}' not found for base_services entry '{reference}'" + ) + return candidate + + def _collect_lineage_includes(self, lineage: List[str]) -> List[str]: + includes: List[str] = [] + for service_full_name in lineage: + service, _ = self._context.service_index[service_full_name] + includes.extend( + _OptionsView(service.options).string_list(self._opts_pb2.extra_includes) + ) + return includes + + @staticmethod + def _snake_case(name: str) -> str: + chars: List[str] = [] + for index, char in enumerate(name): + if ( + char.isupper() + and index > 0 + and ( + not name[index - 1].isupper() + or (index + 1 < len(name) and name[index + 1].islower()) + ) + ): + chars.append("_") + chars.append(char.lower()) + return "".join(chars) + + @staticmethod + def _uniq(values: List[str]) -> List[str]: + return list(dict.fromkeys(values)) diff --git a/alt_core_api/tools/protoc_gen_arduinoif/service_renderer.py b/alt_core_api/tools/protoc_gen_arduinoif/service_renderer.py new file mode 100644 index 000000000..e5eb92300 --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/service_renderer.py @@ -0,0 +1,118 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pathlib import Path +from typing import Iterator, List, Tuple + +from jinja2 import Environment, FileSystemLoader + +from .common import MethodSpec, ServiceModel + +__all__ = [ + "ServiceRenderer", +] + +_TEMPLATES_DIR = Path(__file__).resolve().parent / "templates" +_ENV = Environment( + loader=FileSystemLoader(str(_TEMPLATES_DIR)), + autoescape=False, + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=True, +) + +_SURFACES = { + "ifc": ("ifc_header.hpp.j2", "include_list", "in_ifc"), + "api": ("api_header.hpp.j2", "api_includes", "in_api"), + "service": ("service_header.hpp.j2", "service_includes", "in_service"), + "service_impl": ( + "service_impl_header.hpp.j2", + "service_impl_includes", + "in_service_impl", + ), +} + +_HEADER_ORDER = [ + ("ifc_header", "ifc", None), + ("api_header", "api", "generate_api"), + ("service_header", "service", "generate_service"), + ("service_impl_header", "service_impl", "generate_service_impl"), +] + + +class ServiceRenderer: + def __init__(self, model: ServiceModel) -> None: + self._model = model + + def __iter__(self) -> Iterator[Tuple[str, str]]: + for header_attr, surface, gate_attr in _HEADER_ORDER: + if gate_attr and not getattr(self._model, gate_attr): + continue + yield getattr(self._model, header_attr), self._render_surface(surface) + + @staticmethod + def _render_template(template_name: str, **context) -> str: + rendered = _ENV.get_template(template_name).render(**context) + return rendered if rendered.endswith("\n") else f"{rendered}\n" + + def _render_surface(self, surface: str) -> str: + template, include_attr, method_attr = _SURFACES[surface] + public_methods, protected_methods, private_methods = ( + ServiceRenderer._group_surface_methods(self._model, method_attr) + ) + + return ServiceRenderer._render_template( + template, + includes=getattr(self._model, include_attr), + namespace_name=self._model.namespace_name, + public_methods=public_methods, + protected_methods=protected_methods, + private_methods=private_methods, + **self._surface_context(surface), + ) + + def _surface_context(self, surface: str) -> dict: + if surface == "ifc": + return { + "proto_enums": self._model.proto_enums, + "ifc_name": self._model.ifc_name, + } + if surface == "api": + return { + "ifc_name": self._model.ifc_name, + "api_name": self._model.api_name, + } + if surface == "service": + return { + "service_name": self._model.service_name, + "service_base_ifc_class_names": self._model.service_base_ifc_class_names, + } + return { + "generate_api": self._model.generate_api, + "api_name": self._model.api_name, + "service_name": self._model.service_name, + "service_impl_name": self._model.service_impl_name, + "api_member_name": self._model.api_member_name, + } + + @staticmethod + def _group_surface_methods( + model, + method_attr: str, + ) -> Tuple[List[MethodSpec], List[MethodSpec], List[MethodSpec]]: + public_methods: List[MethodSpec] = [] + protected_methods: List[MethodSpec] = [] + private_methods: List[MethodSpec] = [] + + for planned in model.methods: + if not getattr(planned, method_attr): + continue + if planned.spec.visibility == "protected": + protected_methods.append(planned.spec) + elif planned.spec.visibility == "private": + private_methods.append(planned.spec) + else: + public_methods.append(planned.spec) + + return public_methods, protected_methods, private_methods diff --git a/alt_core_api/tools/protoc_gen_arduinoif/templates/api_header.hpp.j2 b/alt_core_api/tools/protoc_gen_arduinoif/templates/api_header.hpp.j2 new file mode 100644 index 000000000..7d30195c8 --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/templates/api_header.hpp.j2 @@ -0,0 +1,60 @@ +// Generated by protoc-gen-arduinoif. DO NOT EDIT. + +#pragma once + +#include +#include +{% for include in includes %} +#include <{{ include }}> +{% endfor %} +{% if namespace_name %} +namespace {{ namespace_name }} { + +{% endif %} +class {{ api_name }} : public {{ ifc_name }} { +public: + explicit {{ api_name }}({{ ifc_name }}& impl) : impl_(impl) {} + +{% for spec in public_methods %} + {{ spec.decl }} { +{% if spec.returns_void %} + impl_.{{ spec.call_name }}({{ spec.arg_names | join(', ') }}); +{% else %} + return impl_.{{ spec.call_name }}({{ spec.arg_names | join(', ') }}); +{% endif %} + } + +{% endfor %} +{% if protected_methods %} +protected: +{% for spec in protected_methods %} + {{ spec.decl }} { +{% if spec.returns_void %} + impl_.{{ spec.call_name }}({{ spec.arg_names | join(', ') }}); +{% else %} + return impl_.{{ spec.call_name }}({{ spec.arg_names | join(', ') }}); +{% endif %} + } + +{% endfor %} +{% endif %} +{% if private_methods %} +private: +{% for spec in private_methods %} + {{ spec.decl }} { +{% if spec.returns_void %} + impl_.{{ spec.call_name }}({{ spec.arg_names | join(', ') }}); +{% else %} + return impl_.{{ spec.call_name }}({{ spec.arg_names | join(', ') }}); +{% endif %} + } + +{% endfor %} +{% endif %} +private: + {{ ifc_name }}& impl_; +}; + +{% if namespace_name %} +} // namespace {{ namespace_name }} +{% endif %} diff --git a/alt_core_api/tools/protoc_gen_arduinoif/templates/ifc_header.hpp.j2 b/alt_core_api/tools/protoc_gen_arduinoif/templates/ifc_header.hpp.j2 new file mode 100644 index 000000000..bf91f7ef6 --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/templates/ifc_header.hpp.j2 @@ -0,0 +1,45 @@ +// Generated by protoc-gen-arduinoif. DO NOT EDIT. + +#pragma once + +#include +#include +{% for include in includes %} +#include <{{ include }}> +{% endfor %} +{% if namespace_name %} +namespace {{ namespace_name }} { + +{% endif %} +{% for enum_desc in proto_enums %} +typedef enum {{ enum_desc.name }} { +{% for value in enum_desc.value %} + {{ value.name }} = {{ value.number }}, +{% endfor %} +} {{ enum_desc.name }}; + +{% endfor %} +class {{ ifc_name }} { +public: + virtual ~{{ ifc_name }}() = default; + +{% for spec in public_methods %} + virtual {{ spec.decl }} = 0; +{% endfor %} +{% if protected_methods %} +protected: +{% for spec in protected_methods %} + virtual {{ spec.decl }} = 0; +{% endfor %} +{% endif %} +{% if private_methods %} +private: +{% for spec in private_methods %} + virtual {{ spec.decl }} = 0; +{% endfor %} +{% endif %} +}; + +{% if namespace_name %} +} // namespace {{ namespace_name }} +{% endif %} diff --git a/alt_core_api/tools/protoc_gen_arduinoif/templates/service_header.hpp.j2 b/alt_core_api/tools/protoc_gen_arduinoif/templates/service_header.hpp.j2 new file mode 100644 index 000000000..b74c04198 --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/templates/service_header.hpp.j2 @@ -0,0 +1,37 @@ +// Generated by protoc-gen-arduinoif. DO NOT EDIT. + +#pragma once + +#include +#include +{% for include in includes %} +#include <{{ include }}> +{% endfor %} +{% if namespace_name %} +namespace {{ namespace_name }} { + +{% endif %} +class {{ service_name }}{% if service_base_ifc_class_names %} : {% for base_class in service_base_ifc_class_names %}public {{ base_class }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} { +public: + virtual ~{{ service_name }}() = default; + +{% for spec in public_methods %} + virtual {{ spec.decl }} = 0; +{% endfor %} +{% if protected_methods %} +protected: +{% for spec in protected_methods %} + virtual {{ spec.decl }} = 0; +{% endfor %} +{% endif %} +{% if private_methods %} +private: +{% for spec in private_methods %} + virtual {{ spec.decl }} = 0; +{% endfor %} +{% endif %} +}; + +{% if namespace_name %} +} // namespace {{ namespace_name }} +{% endif %} diff --git a/alt_core_api/tools/protoc_gen_arduinoif/templates/service_impl_header.hpp.j2 b/alt_core_api/tools/protoc_gen_arduinoif/templates/service_impl_header.hpp.j2 new file mode 100644 index 000000000..af164c4fb --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/templates/service_impl_header.hpp.j2 @@ -0,0 +1,64 @@ +// Generated by protoc-gen-arduinoif. DO NOT EDIT. + +#pragma once + +#include +#include +{% for include in includes %} +#include <{{ include }}> +{% endfor %} +{% if namespace_name %} +namespace {{ namespace_name }} { + +{% endif %} +{% if not generate_api %} +class {{ api_name }}; + +{% endif %} +class {{ service_impl_name }} : public {{ service_name }} { +public: + explicit {{ service_impl_name }}({{ api_name }}& api) : {{ api_member_name }}(api) {} + +{% for spec in public_methods %} + {{ spec.decl }} override { +{% if spec.returns_void %} + {{ api_member_name }}.{{ spec.call_name }}({{ spec.arg_names | join(', ') }}); +{% else %} + return {{ api_member_name }}.{{ spec.call_name }}({{ spec.arg_names | join(', ') }}); +{% endif %} + } + +{% endfor %} +{% if protected_methods %} +protected: +{% for spec in protected_methods %} + {{ spec.decl }} override { +{% if spec.returns_void %} + {{ api_member_name }}.{{ spec.call_name }}({{ spec.arg_names | join(', ') }}); +{% else %} + return {{ api_member_name }}.{{ spec.call_name }}({{ spec.arg_names | join(', ') }}); +{% endif %} + } + +{% endfor %} +{% endif %} +{% if private_methods %} +private: +{% for spec in private_methods %} + {{ spec.decl }} override { +{% if spec.returns_void %} + {{ api_member_name }}.{{ spec.call_name }}({{ spec.arg_names | join(', ') }}); +{% else %} + return {{ api_member_name }}.{{ spec.call_name }}({{ spec.arg_names | join(', ') }}); +{% endif %} + } + +{% endfor %} +{% endif %} +private: + {{ api_name }}& {{ api_member_name }}; +}; + +{% if namespace_name %} +} // namespace {{ namespace_name }} +{% endif %} diff --git a/alt_core_api/tools/protoc_gen_arduinoif/tests/conftest.py b/alt_core_api/tools/protoc_gen_arduinoif/tests/conftest.py new file mode 100644 index 000000000..aac708bf3 --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/tests/conftest.py @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import os +import subprocess +import tempfile +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[3] +PROTO_DIR = REPO_ROOT / "idl/proto" +PROTO_FILE = PROTO_DIR / "arduino_opts.proto" +PB2_OUTPUT_DIR = Path(tempfile.gettempdir()) / "protoc_gen_arduinoif_pytest" +PB2_FILE = PB2_OUTPUT_DIR / "arduino_opts_pb2.py" + + +def _ensure_pb2() -> Path: + PB2_OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + if PB2_FILE.exists() and PB2_FILE.stat().st_mtime >= PROTO_FILE.stat().st_mtime: + return PB2_FILE + + subprocess.run( + [ + "protoc", + f"--python_out={PB2_OUTPUT_DIR}", + f"--proto_path={PROTO_DIR}", + str(PROTO_FILE), + ], + cwd=REPO_ROOT, + check=True, + ) + return PB2_FILE + + +TEST_PB2_PATH = _ensure_pb2() +os.environ["PROTOC_GEN_ARDUINOIF_PB2"] = str(TEST_PB2_PATH) + + +@pytest.fixture(scope="session") +def test_pb2_path() -> Path: + return TEST_PB2_PATH diff --git a/alt_core_api/tools/protoc_gen_arduinoif/tests/test_options_reader.py b/alt_core_api/tools/protoc_gen_arduinoif/tests/test_options_reader.py new file mode 100644 index 000000000..9e564de5e --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/tests/test_options_reader.py @@ -0,0 +1,122 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +from google.protobuf.compiler import plugin_pb2 +from google.protobuf.descriptor_pb2 import ( + DescriptorProto, + FieldDescriptorProto, + FileDescriptorSet, + MethodDescriptorProto, + ServiceDescriptorProto, +) + +REPO_ROOT = Path(__file__).resolve().parents[3] +sys.path.insert(0, str(REPO_ROOT / "tools")) + +from protoc_gen_arduinoif.common import get_arduino_opts_pb2 # noqa: E402 +from protoc_gen_arduinoif.request_context import RequestContext # noqa: E402 +from protoc_gen_arduinoif.service_model_builder import ServiceModelBuilder # noqa: E402 + +arduino_opts_pb2 = get_arduino_opts_pb2() + + +def _build_request(tmp_path: Path) -> plugin_pb2.CodeGeneratorRequest: + descriptor_path = tmp_path / "input.desc" + proto_dir = REPO_ROOT / "idl/proto" + google_dir = proto_dir / "google/protobuf" + proto_files = [ + "print.proto", + "stream.proto", + "hardware_serial.proto", + "common.proto", + ] + + subprocess.run( + [ + "protoc", + f"--descriptor_set_out={descriptor_path}", + "--include_imports", + f"--proto_path={proto_dir}", + f"--proto_path={google_dir}", + *proto_files, + ], + cwd=REPO_ROOT, + check=True, + ) + + descriptor_set = FileDescriptorSet() + descriptor_set.ParseFromString(descriptor_path.read_bytes()) + + request = plugin_pb2.CodeGeneratorRequest() + request.file_to_generate.extend(proto_files) + request.proto_file.extend(descriptor_set.file) + return request + + +def test_builder_reads_service_and_method_extensions(tmp_path: Path) -> None: + request = _build_request(tmp_path) + context = RequestContext.build(request) + + hardware_serial_file = next( + proto_file + for proto_file in request.proto_file + if proto_file.name == "hardware_serial.proto" + ) + service = next(s for s in hardware_serial_file.service if s.name == "HardwareSerial") + + model = ServiceModelBuilder.build( + service, + hardware_serial_file.package, + list(hardware_serial_file.enum_type), + context, + ) + + assert model.generate_api is True + assert model.api_name == "arduino::HardwareSerial" + + ifc_decls = [planned.spec.decl for planned in model.methods if planned.in_ifc] + assert "void begin(unsigned long arg0)" in ifc_decls + assert "void begin(unsigned long arg0, uint16_t arg1)" in ifc_decls + + +def test_builder_reads_field_extensions_from_input_message() -> None: + input_message = DescriptorProto(name="Input") + field = input_message.field.add() + field.name = "value" + field.number = 1 + field.type = FieldDescriptorProto.TYPE_UINT32 + field.options.Extensions[arduino_opts_pb2.cpp_type] = "uint8_t" + field.options.Extensions[arduino_opts_pb2.field_cpp_name] = "input_value" + + empty_message = DescriptorProto(name="Empty") + + method = MethodDescriptorProto( + name="Foo", + input_type=".test.Input", + output_type=".test.Empty", + ) + service = ServiceDescriptorProto(name="Sample") + service.method.extend([method]) + + context = RequestContext( + message_map={ + ".test.Input": input_message, + ".test.Empty": empty_message, + }, + service_index={ + ".test.Sample": (service, "test"), + }, + lineage_cache={}, + requested_files=set(), + requested_basenames=set(), + ) + + model = ServiceModelBuilder.build(service, "test", [], context) + ifc_specs = [planned.spec for planned in model.methods if planned.in_ifc] + assert len(ifc_specs) == 1 + assert ifc_specs[0].decl == "void Foo(uint8_t input_value)" diff --git a/alt_core_api/tools/protoc_gen_arduinoif/tests/test_options_runtime.py b/alt_core_api/tools/protoc_gen_arduinoif/tests/test_options_runtime.py new file mode 100644 index 000000000..9beec6674 --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/tests/test_options_runtime.py @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from protoc_gen_arduinoif.common import get_arduino_opts_pb2 + + +def test_get_arduino_opts_pb2_requires_env(monkeypatch: pytest.MonkeyPatch) -> None: + get_arduino_opts_pb2.cache_clear() + monkeypatch.delenv("PROTOC_GEN_ARDUINOIF_PB2", raising=False) + + with pytest.raises(RuntimeError, match="PROTOC_GEN_ARDUINOIF_PB2 is not set"): + get_arduino_opts_pb2() + + +def test_get_arduino_opts_pb2_loads_from_env( + monkeypatch: pytest.MonkeyPatch, + test_pb2_path: Path, +) -> None: + get_arduino_opts_pb2.cache_clear() + monkeypatch.setenv("PROTOC_GEN_ARDUINOIF_PB2", str(test_pb2_path)) + + module = get_arduino_opts_pb2() + assert hasattr(module, "cpp_name") diff --git a/alt_core_api/tools/protoc_gen_arduinoif/tests/test_renderer_templates.py b/alt_core_api/tools/protoc_gen_arduinoif/tests/test_renderer_templates.py new file mode 100644 index 000000000..a7f30626a --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/tests/test_renderer_templates.py @@ -0,0 +1,131 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import sys +from pathlib import Path +from typing import NamedTuple + +from google.protobuf.descriptor_pb2 import EnumDescriptorProto + +REPO_ROOT = Path(__file__).resolve().parents[3] +sys.path.insert(0, str(REPO_ROOT / "tools")) + +from protoc_gen_arduinoif.common import ( # noqa: E402 + MethodSpec, + ServiceModel, +) +from protoc_gen_arduinoif.service_renderer import ServiceRenderer # noqa: E402 + + +class _PlannedMethod(NamedTuple): + spec: MethodSpec + in_ifc: bool + in_api: bool + in_service: bool + in_service_impl: bool + + +def _sample_model() -> ServiceModel: + enum_desc = EnumDescriptorProto(name="Mode") + enum_desc.value.add(name="MODE_A", number=0) + + foo_spec = MethodSpec( + decl="int foo(uint8_t arg0)", + call_name="foo", + arg_names=["arg0"], + returns_void=False, + source_virtual=True, + emit_api=True, + emit_service=True, + visibility="public", + ) + bar_spec = MethodSpec( + decl="void bar()", + call_name="bar", + arg_names=[], + returns_void=True, + source_virtual=True, + emit_api=True, + emit_service=True, + visibility="protected", + ) + baz_spec = MethodSpec( + decl="void baz()", + call_name="baz", + arg_names=[], + returns_void=True, + source_virtual=True, + emit_api=True, + emit_service=True, + visibility="private", + ) + + methods = [ + _PlannedMethod(foo_spec, True, True, True, True), + _PlannedMethod(bar_spec, True, True, True, True), + _PlannedMethod(baz_spec, True, True, True, True), + ] + + return ServiceModel( + include_list=["ifc_dep.hpp"], + api_includes=["api_dep.hpp"], + service_includes=["service_dep.hpp"], + service_impl_includes=["impl_dep.hpp"], + namespace_name="arduino", + proto_enums=[enum_desc], + ifc_name="ExampleInterface", + api_name="ExampleApi", + service_name="ExampleService", + service_impl_name="ExampleServiceImpl", + api_member_name="api_", + service_base_ifc_class_names=["BaseIfc"], + ifc_header="example_interface.hpp", + api_header="example_api.hpp", + service_header="example_service.hpp", + service_impl_header="example_service_impl.hpp", + methods=methods, + generate_api=True, + generate_service=True, + generate_service_impl=True, + ) + + +def test_renderer_templates_emit_expected_fragments() -> None: + model = _sample_model() + renderer = ServiceRenderer(model) + rendered = dict(renderer) + + ifc_content = rendered["example_interface.hpp"] + assert "class ExampleInterface" in ifc_content + assert "virtual int foo(uint8_t arg0) = 0;" in ifc_content + assert "typedef enum Mode" in ifc_content + assert "namespace arduino" in ifc_content + + api_content = rendered["example_api.hpp"] + assert "class ExampleApi : public ExampleInterface" in api_content + assert "return impl_.foo(arg0);" in api_content + assert "impl_.bar();" in api_content + + service_content = rendered["example_service.hpp"] + assert "class ExampleService : public BaseIfc" in service_content + assert "virtual void baz() = 0;" in service_content + + impl_content = rendered["example_service_impl.hpp"] + assert "class ExampleServiceImpl : public ExampleService" in impl_content + assert "return api_.foo(arg0);" in impl_content + assert "api_.baz();" in impl_content + assert "override" in impl_content + + +def test_renderer_template_iter_order_is_stable() -> None: + model = _sample_model() + renderer = ServiceRenderer(model) + + names = [name for name, _ in renderer] + assert names == [ + "example_interface.hpp", + "example_api.hpp", + "example_service.hpp", + "example_service_impl.hpp", + ] diff --git a/alt_core_api/tools/protoc_gen_arduinoif/tests/test_request_context.py b/alt_core_api/tools/protoc_gen_arduinoif/tests/test_request_context.py new file mode 100644 index 000000000..08b196b1c --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/tests/test_request_context.py @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import sys +from pathlib import Path + +from google.protobuf.compiler import plugin_pb2 +from google.protobuf.descriptor_pb2 import DescriptorProto + +REPO_ROOT = Path(__file__).resolve().parents[3] +sys.path.insert(0, str(REPO_ROOT / "tools")) + +from protoc_gen_arduinoif.common import full_service_name # noqa: E402 +from protoc_gen_arduinoif.request_context import RequestContext # noqa: E402 + + +def _build_request() -> plugin_pb2.CodeGeneratorRequest: + request = plugin_pb2.CodeGeneratorRequest() + request.file_to_generate.extend( + [ + "idl/proto/print.proto", + "common.proto", + ] + ) + + print_file = request.proto_file.add() + print_file.name = "idl/proto/print.proto" + print_file.package = "arduino" + outer = DescriptorProto(name="Outer") + outer.nested_type.extend([DescriptorProto(name="Inner")]) + print_file.message_type.extend([outer]) + print_file.service.add().name = "Print" + + common_file = request.proto_file.add() + common_file.name = "common.proto" + common_file.message_type.add().name = "Common" + common_file.service.add().name = "CommonService" + + return request + + +def test_build_request_context_collects_message_and_service_indexes() -> None: + context = RequestContext.build(_build_request()) + + assert ".arduino.Outer" in context.message_map + assert ".arduino.Outer.Inner" in context.message_map + assert ".Common" in context.message_map + + assert ".arduino.Print" in context.service_index + assert ".CommonService" in context.service_index + + assert context.lineage_cache == {} + + +def test_full_service_name_handles_package_and_root() -> None: + assert full_service_name("arduino", "Print") == ".arduino.Print" + assert full_service_name("", "CommonService") == ".CommonService" + + +def test_is_requested_proto_matches_path_and_basename() -> None: + context = RequestContext.build(_build_request()) + + assert context.is_requested_proto("idl/proto/print.proto") + assert context.is_requested_proto("print.proto") + assert context.is_requested_proto("common.proto") + assert not context.is_requested_proto("idl/proto/stream.proto") diff --git a/alt_core_api/tools/protoc_gen_arduinoif/tests/test_service_model.py b/alt_core_api/tools/protoc_gen_arduinoif/tests/test_service_model.py new file mode 100644 index 000000000..b26608c9d --- /dev/null +++ b/alt_core_api/tools/protoc_gen_arduinoif/tests/test_service_model.py @@ -0,0 +1,147 @@ +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +from google.protobuf.compiler import plugin_pb2 +from google.protobuf.descriptor_pb2 import ( + DescriptorProto, + FileDescriptorSet, + MethodDescriptorProto, + ServiceDescriptorProto, +) + +REPO_ROOT = Path(__file__).resolve().parents[3] +sys.path.insert(0, str(REPO_ROOT / "tools")) + +from protoc_gen_arduinoif.common import get_arduino_opts_pb2 # noqa: E402 +from protoc_gen_arduinoif.request_context import RequestContext # noqa: E402 +from protoc_gen_arduinoif.service_model_builder import ServiceModelBuilder # noqa: E402 +from protoc_gen_arduinoif.service_renderer import ServiceRenderer # noqa: E402 + +arduino_opts_pb2 = get_arduino_opts_pb2() + + +def _build_request(tmp_path: Path) -> plugin_pb2.CodeGeneratorRequest: + descriptor_path = tmp_path / "input.desc" + proto_dir = REPO_ROOT / "idl/proto" + google_dir = proto_dir / "google/protobuf" + proto_files = [ + "print.proto", + "stream.proto", + "hardware_serial.proto", + "common.proto", + ] + + command = [ + "protoc", + f"--descriptor_set_out={descriptor_path}", + "--include_imports", + f"--proto_path={proto_dir}", + f"--proto_path={google_dir}", + *proto_files, + ] + subprocess.run(command, cwd=REPO_ROOT, check=True) + + descriptor_set = FileDescriptorSet() + descriptor_set.ParseFromString(descriptor_path.read_bytes()) + + request = plugin_pb2.CodeGeneratorRequest() + request.file_to_generate.extend(proto_files) + request.proto_file.extend(descriptor_set.file) + return request + + +def _hardware_serial_model(tmp_path: Path): + request = _build_request(tmp_path) + context = RequestContext.build(request) + + target_file = next( + proto_file + for proto_file in request.proto_file + if proto_file.name == "hardware_serial.proto" + ) + target_service = next( + service for service in target_file.service if service.name == "HardwareSerial" + ) + return ServiceModelBuilder.build( + target_service, + target_file.package, + list(target_file.enum_type), + context, + ) + + +def test_build_service_model_has_expected_headers_and_groups(tmp_path: Path) -> None: + model = _hardware_serial_model(tmp_path) + + assert model.ifc_header == "hardware_serial_interface.hpp" + assert model.api_header == "hardware_serial_api.hpp" + assert model.service_header == "hardware_serial_service.hpp" + assert model.service_impl_header == "hardware_serial_service_impl.hpp" + assert model.generate_api is True + assert model.generate_service is True + assert model.generate_service_impl is True + + ifc_methods = [planned.spec for planned in model.methods if planned.in_ifc] + ifc_call_names = [spec.call_name for spec in ifc_methods] + assert ifc_call_names == ["begin", "begin", "end", "operator bool"] + assert all(spec.visibility == "public" for spec in ifc_methods) + + +def test_render_service_headers_uses_stable_order(tmp_path: Path) -> None: + model = _hardware_serial_model(tmp_path) + rendered_names = [name for name, _ in ServiceRenderer(model)] + assert rendered_names == [ + "hardware_serial_interface.hpp", + "hardware_serial_api.hpp", + "hardware_serial_service.hpp", + "hardware_serial_service_impl.hpp", + ] + + +def test_build_service_model_prefers_latest_duplicate_decl() -> None: + empty_message = DescriptorProto(name="Empty") + message_map = {".test.Empty": empty_message} + + base_method = MethodDescriptorProto( + name="Ping", + input_type=".test.Empty", + output_type=".test.Empty", + ) + child_method = MethodDescriptorProto( + name="Ping", + input_type=".test.Empty", + output_type=".test.Empty", + ) + child_method.options.Extensions[arduino_opts_pb2.method_visibility] = "protected" + + base_service = ServiceDescriptorProto(name="Base") + base_service.method.extend([base_method]) + child_service = ServiceDescriptorProto(name="Child") + child_service.options.Extensions[arduino_opts_pb2.base_services].append("Base") + child_service.method.extend([child_method]) + + service_index = { + ".test.Base": (base_service, "test"), + ".test.Child": (child_service, "test"), + } + context = RequestContext( + message_map=message_map, + service_index=service_index, + lineage_cache={}, + requested_files=set(), + requested_basenames=set(), + ) + model = ServiceModelBuilder.build( + child_service, + "test", + [], + context, + ) + specs = [planned.spec for planned in model.methods] + assert len(specs) == 1 + assert specs[0].visibility == "protected" diff --git a/cores/arduino/CMakeLists.txt b/cores/arduino/CMakeLists.txt index 36e337b90..0455376dd 100644 --- a/cores/arduino/CMakeLists.txt +++ b/cores/arduino/CMakeLists.txt @@ -1,11 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# This line ensures that cores/arduino/zephyr/api is loaded ahead of -# cores/arduino/api. -if(CONFIG_USE_ARDUINO_API_RUST_IMPLEMENTATION) - zephyr_include_directories(zephyr) -endif() - zephyr_include_directories(.) zephyr_include_directories(../../variants) @@ -14,14 +8,8 @@ if(NOT DEFINED ARDUINO_BUILD_PATH) zephyr_sources(zephyrSerial.cpp) zephyr_sources(zephyrCommon.cpp) -if(CONFIG_USE_ARDUINO_API_RUST_IMPLEMENTATION) - zephyr_sources(zephyrPrint.cpp) - zephyr_sources(apiCommon.cpp) -endif() - if(DEFINED CONFIG_ARDUINO_ENTRY) zephyr_sources(main.cpp) endif() endif() - diff --git a/cores/arduino/apiCommon.cpp b/cores/arduino/apiCommon.cpp deleted file mode 100644 index cfb6d919f..000000000 --- a/cores/arduino/apiCommon.cpp +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2025 TOKITA Hiroshi - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include -#include "zephyrInternal.h" - -extern "C" { - int32_t map_i32(int32_t x, int32_t in_min, int32_t in_max, int32_t out_min, int32_t out_max); - uint16_t makeWord_w(uint16_t w); - uint16_t makeWord_hl(byte h, byte l); -} - -long map(long x, long in_min, long in_max, long out_min, long out_max) -{ - return map_i32(x, in_min, in_max, out_min, out_max); -} - -uint16_t makeWord(uint16_t w) { - return makeWord_w(w); -} -uint16_t makeWord(byte h, byte l) { - return makeWord_hl(h, l); -} diff --git a/cores/arduino/zephyrCommon.cpp b/cores/arduino/zephyrCommon.cpp index 6b011c078..3e3ea4e38 100644 --- a/cores/arduino/zephyrCommon.cpp +++ b/cores/arduino/zephyrCommon.cpp @@ -315,7 +315,7 @@ void tone(pin_size_t pinNumber, unsigned int frequency, key = k_spin_lock(&pt->lock); pt->infinity = (duration == 0); - pt->count = min((uint64_t)duration * frequency * TOGGLES_PER_CYCLE / MSEC_PER_SEC, UINT32_MAX); + pt->count = min((uint64_t)duration * frequency * TOGGLES_PER_CYCLE / MSEC_PER_SEC, (uint64_t)UINT32_MAX); pt->pin = pinNumber; k_spin_unlock(&pt->lock, key); diff --git a/cores/arduino/zephyrPrint.h b/cores/arduino/zephyrPrint.h deleted file mode 100644 index 7b6131b6e..000000000 --- a/cores/arduino/zephyrPrint.h +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (c) 2022 TOKITA Hiroshi - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include - -#include -#include - -namespace arduino -{ -namespace zephyr -{ - -int cbprintf_callback(int c, void *ctx); -size_t wrap_cbprintf(void *ctx, const char *format, ...); -size_t print_number_base_any(void *ctx, unsigned long long ull, int base); -size_t print_number_base_pow2(void *ctx, unsigned long long ull, unsigned bits); - -template size_t print_number(void *ctx, Number n, const int base, const char *decfmt) -{ - if (base == 0) { - return reinterpret_cast(ctx)->write((char)n); - } else if (base == 2) { - return arduino::zephyr::print_number_base_pow2(ctx, n, 1); - } else if (base == 4) { - return arduino::zephyr::print_number_base_pow2(ctx, n, 2); - } else if (base == 8) { - return arduino::zephyr::print_number_base_pow2(ctx, n, 3); - } else if (base == 10) { - return arduino::zephyr::wrap_cbprintf(ctx, decfmt, n); - } else if (base == 16) { - return arduino::zephyr::print_number_base_pow2(ctx, n, 4); - } else if (base == 32) { - return arduino::zephyr::print_number_base_pow2(ctx, n, 5); - } else { - return arduino::zephyr::print_number_base_any(ctx, n, base); - } -} - -} // namespace zephyr - -} // namespace arduino - -inline size_t arduino::Print::print(const __FlashStringHelper *fsh) -{ - return write(reinterpret_cast(fsh)); -} - -inline size_t arduino::Print::print(const String &s) -{ - return write(s.c_str(), s.length()); -} - -inline size_t arduino::Print::print(const char str[]) -{ - return write(str); -} - -inline size_t arduino::Print::print(char c) -{ - return write(c); -} - -inline size_t arduino::Print::print(unsigned char n, int base) -{ - return arduino::zephyr::print_number(this, n, base, "%hhu"); -} - -inline size_t arduino::Print::print(int n, int base) -{ - return arduino::zephyr::print_number(this, n, base, "%d"); -} - -inline size_t arduino::Print::print(unsigned int n, int base) -{ - return arduino::zephyr::print_number(this, n, base, "%u"); -} - -inline size_t arduino::Print::print(long n, int base) -{ - return arduino::zephyr::print_number(this, n, base, "%ld"); -} - -inline size_t arduino::Print::print(unsigned long n, int base) -{ - return arduino::zephyr::print_number(this, n, base, "%lu"); -} - -inline size_t arduino::Print::print(long long n, int base) -{ - return arduino::zephyr::print_number(this, n, base, "%lld"); -} - -inline size_t arduino::Print::print(unsigned long long n, int base) -{ - return arduino::zephyr::print_number(this, n, base, "%llu"); -} - -inline size_t arduino::Print::print(double n, int perception) -{ - if (perception < 10) { - const char ch_perception = static_cast('0' + perception); - const char format[] = {'%', '.', ch_perception, 'f', '\0'}; - return arduino::zephyr::wrap_cbprintf(this, format, n); - } else { - const char ch_perception = static_cast('0' + (perception % 10)); - const char format[] = {'%', '.', '1', ch_perception, 'f', '\0'}; - return arduino::zephyr::wrap_cbprintf(this, format, n); - } -} - -inline size_t arduino::Print::print(const Printable &printable) -{ - return printable.printTo(*this); -} - -inline size_t arduino::Print::println(const __FlashStringHelper *fsh) -{ - return print(fsh) + println(); -} - -inline size_t arduino::Print::println(const String &s) -{ - return print(s) + println(); -} - -inline size_t arduino::Print::println(const char str[]) -{ - return print(str) + println(); -} - -inline size_t arduino::Print::println(char c) -{ - return print(c) + println(); -} - -inline size_t arduino::Print::println(unsigned char uc, int base) -{ - return print(uc, base) + println(); -} - -inline size_t arduino::Print::println(int i, int base) -{ - return print(i, base) + println(); -} - -inline size_t arduino::Print::println(unsigned int ui, int base) -{ - return print(ui, base) + println(); -} - -inline size_t arduino::Print::println(long l, int base) -{ - return print(l, base) + println(); -} - -inline size_t arduino::Print::println(unsigned long ul, int base) -{ - return print(ul, base) + println(); -} - -inline size_t arduino::Print::println(long long ll, int base) -{ - return print(ll, base) + println(); -} - -inline size_t arduino::Print::println(unsigned long long ull, int base) -{ - return print(ull, base) + println(); -} - -inline size_t arduino::Print::println(double d, int perception) -{ - return print(d, perception) + println(); -} - -inline size_t arduino::Print::println(const Printable &printable) -{ - return print(printable) + println(); -} - -inline size_t arduino::Print::println(void) -{ - return write("\r\n", 2); -} diff --git a/documentation/idl_codegen.md b/documentation/idl_codegen.md new file mode 100644 index 000000000..7a9be6c3c --- /dev/null +++ b/documentation/idl_codegen.md @@ -0,0 +1,188 @@ +# Arduino IDL Codegen Manual + +This document explains how to use `protoc-gen-arduinoif` with the Arduino IDL `.proto` files in `idl/proto`. + +## Goal + +Generate C++ headers from proto service definitions with explicit control over: + +1. `Ifc` class (pure virtual, virtual-source methods only) +2. `Api` class (delegate wrapper) +3. `Service` class (pure virtual service contract) +4. `ServiceImpl` class (service implementation delegating to `Api`) + +The generator supports per-method and per-service options in `arduino_opts.proto`. + +## Files + +1. Generator: `alt_core_api/tools/protoc-gen-arduinoif` +2. Options: `alt_core_api/idl/proto/arduino_opts.proto` +3. IDL services: `alt_core_api/idl/proto/*.proto` + +## Build Integration + +`alt_core_api/idl/CMakeLists.txt` already invokes the plugin and emits headers into the build tree. +The build also generates `arduino_opts_pb2.py` and passes its path via `PROTOC_GEN_ARDUINOIF_PB2`. +The plugin requires this environment variable and does not generate pb2 at runtime. + +## Processing Model + +The generator uses a three-stage pipeline: + +1. Build `RequestContext` from `CodeGeneratorRequest` (`message_map`, `service_index`, requested file sets, lineage cache). +2. Build `ServiceModel` IR per service from descriptors, options, lineage, and method specs. +3. Stream render headers from each model in order: `ifc -> api -> service -> service_impl`. + +`ServiceModel` stores methods as a single list with per-surface flags (`in_ifc`, `in_api`, `in_service`, `in_service_impl`) to avoid duplicating method collections. + +Only `RequestContext` is global. Service models are built and emitted one-by-one. + +Module visibility is intentionally constrained: + +1. `request_context.py` exposes `RequestContext`. +2. `common.py` exposes shared utilities/types (`full_service_name`, `get_arduino_opts_pb2`, `ServiceModel`, `MethodSpec`). +3. `service_model_builder.py` exposes `ServiceModelBuilder`; helper types used there are private. +4. `service_renderer.py` exposes `ServiceRenderer` for header rendering. + +Typical generated header names (per service): + +1. `*_interface.hpp` +2. `*_api.hpp` +3. `*_service.hpp` +4. `*_service_impl.hpp` + +## Method Options + +Defined in `alt_core_api/idl/proto/arduino_opts.proto` as `google.protobuf.MethodOptions` extensions: + +1. `cpp_name` +2. `cpp_return` +3. `cpp_arg_types` +4. `source_virtual` +5. `emit_api` +6. `emit_service` +7. `method_visibility` + +### Meaning + +1. `cpp_name` + C++ method name. If omitted, RPC name is used. +2. `cpp_return` + C++ return type override. If omitted, output message is inferred (`0 field -> void`, `1 field -> field type`, otherwise `void`). +3. `cpp_arg_types` + Optional repeated list of C++ argument types. If present, these types are used with auto names (`arg0`, `arg1`, ...), and input message fields are ignored for arguments. +4. `source_virtual` + Marks whether this method is considered virtual in the source Arduino class model. +5. `emit_api` + Whether this method is emitted to generated `Api`. +6. `emit_service` + Whether this method is emitted to generated `Service`. +7. `method_visibility` + Access specifier for generated methods. Supported values: `public`, `protected`, `private` (default: `public`). + +## Service Options + +Defined as `google.protobuf.ServiceOptions` extensions: + +1. `generate_api_class` +2. `generate_service_class` +3. `generate_service_impl_class` +4. `ifc_class_name` +5. `api_class_name` +6. `service_class_name` +7. `service_impl_class_name` +8. `api_member_name` +9. `base_services` +10. `ifc_header_name` +11. `extra_includes` + +### Meaning + +1. `generate_api_class` + Emit `Api` delegate wrapper. +2. `generate_service_class` + Emit `Service` pure virtual class. +3. `generate_service_impl_class` + Emit `ServiceImpl` delegating to `Api`. +4. `*_class_name` + Override generated class names. +5. `api_member_name` + Override delegate member name in `ServiceImpl` (default: `api_`). +6. `base_services` + Parent services to inherit from. You can specify a same-package short name (for example `Print`) or a fully-qualified name (for example `arduino.idl.Print` or `.arduino.idl.Print`). Generated `Service` class declarations inherit all ancestor interface classes (direct and indirect) in lineage order. +7. `ifc_header_name` + Override generated interface header name (default: `_interface.hpp`). +8. `extra_includes` + Additional includes emitted to generated headers. + +## Recommended Mapping + +Use this mapping for mixed virtual/non-virtual Arduino APIs: + +1. `Ifc` + Include only methods with `source_virtual = true`. +2. `Api` + Wrapper for `Ifc` operations. +3. `Service` + Contract surface for service exposure. You can include additional methods via `emit_service = true`. +4. `ServiceImpl` + Delegates service calls into an `Api` object. + +## Validation Rules (Fail Fast) + +The generator intentionally fails when configuration is inconsistent: + +1. `generate_service_impl_class = true` requires `generate_service_class = true` +2. `generate_api_class = true` rejects methods with `emit_api = true` and `source_virtual = false` +3. `generate_service_impl_class = true` with `generate_api_class = true` requires all `Service` methods to be callable on generated `Api` +4. Cyclic `base_services` references are rejected + +## Example + +```proto +syntax = "proto3"; +package arduino.idl; + +import "arduino_opts.proto"; +import "google/protobuf/empty.proto"; + +service Demo { + option (arduino.generate_api_class) = true; + option (arduino.generate_service_class) = true; + option (arduino.generate_service_impl_class) = true; + + option (arduino.ifc_class_name) = "DemoIfcCustom"; + option (arduino.api_class_name) = "DemoApiCustom"; + option (arduino.service_class_name) = "DemoServiceCustom"; + option (arduino.service_impl_class_name) = "DemoServiceImplCustom"; + option (arduino.api_member_name) = "delegate_"; + option (arduino.base_services) = "Print"; + + rpc Read(google.protobuf.Empty) returns (google.protobuf.Empty) { + option (arduino.cpp_name) = "read"; + option (arduino.cpp_return) = "int"; + option (arduino.source_virtual) = true; + option (arduino.emit_api) = true; + option (arduino.emit_service) = true; + option (arduino.method_visibility) = "public"; + } +} +``` + +## Command Line Example + +```sh +export PROTOC_GEN_ARDUINOIF_PB2=/path/to/arduino_opts_pb2.py +protoc \ + --plugin=protoc-gen-arduinoif=tools/protoc-gen-arduinoif \ + --arduinoif_out=/tmp/arduinoif-gen \ + --proto_path=idl/proto \ + --proto_path=idl/proto/google/protobuf \ + idl/proto/print.proto +``` + +## Notes + +1. This generator produces headers only. +2. Runtime transport (for example nanopb framing, dispatch loop, registry) is a separate layer. +3. Keep API compatibility decisions in proto options so codegen output remains deterministic. diff --git a/libraries/Wire/CMakeLists.txt b/libraries/Wire/CMakeLists.txt index 231608328..449142ac8 100644 --- a/libraries/Wire/CMakeLists.txt +++ b/libraries/Wire/CMakeLists.txt @@ -3,6 +3,6 @@ zephyr_include_directories(.) if(NOT DEFINED ARDUINO_BUILD_PATH) -zephyr_sources(Wire.cpp) +zephyr_sources_ifdef(CONFIG_I2C Wire.cpp) endif() diff --git a/samples/blinky_arduino/prj.conf b/samples/blinky_arduino/prj.conf index 290d61a3c..509972c3a 100644 --- a/samples/blinky_arduino/prj.conf +++ b/samples/blinky_arduino/prj.conf @@ -1,2 +1,3 @@ CONFIG_GPIO=y CONFIG_ARDUINO_API=y +CONFIG_USE_ARDUINO_API_RUST_IMPLEMENTATION=y diff --git a/samples/hello_arduino/prj.conf b/samples/hello_arduino/prj.conf index f93fa3218..5f99d29f2 100644 --- a/samples/hello_arduino/prj.conf +++ b/samples/hello_arduino/prj.conf @@ -1 +1,3 @@ CONFIG_ARDUINO_API=y +CONFIG_USE_ARDUINO_API_RUST_IMPLEMENTATION=y +CONFIG_I2C=n