From c7ca91a6fd365ffce7ab04f9080943cec6d1d282 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:13:38 +0800 Subject: [PATCH 1/4] hal: C++ API (hal.hh) and pybind11 bindings on the new HAL API Reintroduce a C++ interface for HAL, built strictly on the public C API and the user-land query API: no hal_priv.h, no direct shared memory access, no re-implemented library internals. - hal.hh: type-safe, header-only C++ layer in linuxcnc::hal. Typed pin/param handles via traits over the rtapi_ types (compile-time accessor selection), runtime-typed pin_t variant and anypin for name-based access, component class with add_pin for the struct-member idiom, and ULAPI by-name query/set functions when the query API is present. Handles re-read the shmem slot on every access so hal_link() slot rewrites stay visible (C pointer-variable semantics). Handles are move-only; HAL objects are unique. Query callback paths are exception-free; errors are reported after the library releases the HAL mutex. - halpybind.cc: pybind11 module (halpp.so) exposing component, Pin, enums and the by-name functions. Built alongside _hal/hal.py, replacing nothing. Library init/exit at import; string set values delegate to setps_common_cb for halcmd-consistent parsing. - HAL_PORT intentionally exempted until the API break: the port handle type and creation semantics are still in flux. - tests/halpp: Python and native C++ smoke suites. Based on the pybind11 branch by rene-dev, re-based on the new HAL API. --- debian/control.top.in | 1 + src/Makefile | 1 + src/hal/Submakefile | 26 ++ src/hal/hal.hh | 691 ++++++++++++++++++++++++++++++++-------- src/hal/halpybind.cc | 138 ++++++++ tests/halpp/cpp_test.cc | 90 ++++++ tests/halpp/smoke.py | 99 ++++++ 7 files changed, 919 insertions(+), 127 deletions(-) create mode 100644 src/hal/halpybind.cc create mode 100644 tests/halpp/cpp_test.cc create mode 100644 tests/halpp/smoke.py diff --git a/debian/control.top.in b/debian/control.top.in index 63539badee6..f503e9b9e7d 100644 --- a/debian/control.top.in +++ b/debian/control.top.in @@ -40,6 +40,7 @@ Build-Depends: psmisc, python3, python3-dev, + python3-pybind11, python3-tk, python3-xlib, tcl, diff --git a/src/Makefile b/src/Makefile index 8b6079b62f3..bf5a60b3652 100644 --- a/src/Makefile +++ b/src/Makefile @@ -394,6 +394,7 @@ build-software: headers $(INFILES) # SRCHEADERS := \ hal/hal.h \ + hal/hal.hh \ hal/drivers/mesa-hostmot2/hostmot2-serial.h \ emc/linuxcnc.h \ emc/kinematics/kinematics.h \ diff --git a/src/hal/Submakefile b/src/hal/Submakefile index 1827d7e10c8..86db80104ee 100644 --- a/src/hal/Submakefile +++ b/src/hal/Submakefile @@ -3,6 +3,10 @@ ../include/hal.h: ./hal/hal.h cp $^ $@ +# hal.hh is the C++ interface on top of the public C API +../include/hal.hh: ./hal/hal.hh + cp $^ $@ + HALLIBSRCS := hal/hal_lib.c hal/hal_lib_query.c hal/hal_lib_extra.c $(ULAPISRCS) $(call TOOBJSDEPS, $(HALLIBSRCS)): EXTRAFLAGS += -fPIC $(ULAPI_CFLAGS) USERSRCS += $(HALLIBSRCS) @@ -28,5 +32,27 @@ $(HALMODULE): $(call TOOBJS, $(HALMODULESRCS)) $(HALLIB) $(ECHO) Linking python module $(notdir $@) $(Q)$(CXX) $(LDFLAGS) -shared -o $@ $^ +# pybind11 C++ bindings (hal.hh based) +# setps_util.c only exists in the tree with the HAL query API +# pybind11 headers: python3-pybind11 (system include) or pip user site. +# Without them the bindings are skipped, not failed: they are optional +# until the build dependency is made mandatory. +PYBIND11INCLUDES := $(shell $(PYTHON) -m pybind11 --includes 2>/dev/null) +ifneq ($(PYBIND11INCLUDES),) +HALPPSRCS := hal/halpybind.cc $(wildcard hal/utils/setps_util.c) +PYSRCS += $(HALPPSRCS) + +$(call TOOBJS, $(HALPPSRCS)): EXTRAFLAGS += $(PYBIND11INCLUDES) + +HALPP := ../lib/python/halpp.so +$(HALPP): $(call TOOBJS, $(HALPPSRCS)) $(HALLIB) + $(ECHO) Linking python module $(notdir $@) + $(Q)$(CXX) $(LDFLAGS) -shared -o $@ $^ + +PYTARGETS += $(HALPP) +else +$(info NOTE: pybind11 headers not found, skipping the halpp bindings (install python3-pybind11)) +endif + TARGETS += $(HALLIB) ../lib/liblinuxcnchal.so.0 PYTARGETS += $(HALMODULE) diff --git a/src/hal/hal.hh b/src/hal/hal.hh index e4c9a40e637..c748a39b4ca 100644 --- a/src/hal/hal.hh +++ b/src/hal/hal.hh @@ -1,168 +1,605 @@ +/* + hal.hh - C++ interface for HAL + + A thin, type-safe C++ layer on top of the public HAL C API. + All pin/param access goes through the typed hal_get_X and hal_set_X + accessors and the user-land query API. No direct shared memory + access, no hal_priv.h, no re-implemented library internals. + + Header-only, no runtime overhead: typed access expands to the same + inline accessor calls as the C API. + + The by-name query/set section requires the HAL query API and is + compiled only when the public header defines the COMPONENT_TYPE_* + macros. The typed component/pin layer only needs the base + getter/setter API. + + HAL_PORT pins are intentionally not supported yet. The port handle + type and creation semantics are still in flux until the API break + ("the port change must be done later" in hal.h); a proper port + wrapper follows once hal_port_t and hal_pin_new_port() are final. +*/ #ifndef HALXX_HH #define HALXX_HH #include #include +#include #include -#include "hal.h" +#include +#include +#include +#include -#warning "Do not use hal.hh. It will be removed (and, eventually, replaced)." +#include -enum class hal_dir{ - IN = HAL_IN, +// The HAL query API (hal_get_p, hal_set_p, hal_comp_by_name, ...) is +// present when the public header defines the COMPONENT_TYPE_* macros. +#if defined(COMPONENT_TYPE_REALTIME) && !defined(HALXX_WITH_QUERY_API) +#define HALXX_WITH_QUERY_API 1 +#endif + +#ifndef RTAPI_SINT_MAX +#define RTAPI_SINT_MAX RTAPI_INT64_MAX +#define RTAPI_SINT_MIN RTAPI_INT64_MIN +#define RTAPI_UINT_MAX RTAPI_UINT64_MAX +#endif + +namespace linuxcnc { +namespace hal { + +// Unified pin/param direction. Values are identical to hal_pdir_t. +enum class dir : int { + IN = HAL_IN, OUT = HAL_OUT, + IO = HAL_IO, + RO = HAL_RO, + RW = HAL_RW, }; -#if 0 -// If this class is ever necessary, then it needs to be moved into a new -// header 'hal_priv.hh' because it uses internal access methods from -// 'hal_priv.h' that should not be available to the casual source file. +// Runtime value of a pin, param or signal. Used whenever the HAL type +// is not known at compile time (name-based access, script bindings). +using value_t = std::variant; + +namespace detail { +// Error text that works with and without hal_strerror() in the library. +inline std::string errstr(int rv) +{ +#ifdef HALXX_WITH_QUERY_API + return hal_strerror(rv); +#else + return std::strerror(-rv); +#endif +} +} // namespace detail -#include "hal_priv.h" +//---------------------------------------------------------------------- +// Type traits: map an rtapi_ value type to its HAL handle, HAL type and +// accessor/creator functions. Using an unsupported type is a compile +// error because traits is intentionally left undefined. +// +// The 32-bit versions (rtapi_s32, rtapi_u32) are compatibility types +// that will be retired after the API break; only their traits entries +// and the variant alternatives need to change then. +//---------------------------------------------------------------------- +template struct traits; -class hal{ - public: - static bool component_exists(const std::string& name){ - return halpr_find_comp_by_name(name.c_str()) != NULL; +template<> struct traits { + using handle_t = hal_bool_t; + static handle_t *slot(hal_refs_u *u) { return &u->b; } + static constexpr hal_type_t type = HAL_BOOL; + static rtapi_bool get(handle_t h) { return hal_get_bool(h); } + static rtapi_bool set(handle_t h, rtapi_bool v) { return hal_set_bool(h, v); } + static int new_pin(int c, hal_pdir_t d, handle_t *h, rtapi_bool def, const std::string &n) { + return hal_pin_new_bool(c, d, h, def, "%s", n.c_str()); } - static bool pin_has_writer(const std::string& name){ - hal_pin_t *pin = halpr_find_pin_by_name(name.c_str()); - if(!pin) {//pin does not exist - return false; - } - if(pin->signal) { - hal_sig_t *signal = (hal_sig_t*)SHMPTR(pin->signal); - return signal->writers > 0; - } - return false; + static int new_param(int c, hal_pdir_t d, handle_t *h, rtapi_bool def, const std::string &n) { + return hal_param_new_bool(c, d, h, def, "%s", n.c_str()); + } +}; + +template<> struct traits { + using handle_t = hal_sint_t; + static handle_t *slot(hal_refs_u *u) { return &u->s; } + static constexpr hal_type_t type = HAL_S32; + static rtapi_s32 get(handle_t h) { return hal_get_si32(h); } + static rtapi_s32 set(handle_t h, rtapi_s32 v) { return hal_set_si32(h, v); } + static int new_pin(int c, hal_pdir_t d, handle_t *h, rtapi_s32 def, const std::string &n) { + return hal_pin_new_si32(c, d, h, def, "%s", n.c_str()); } - static bool component_is_ready(const std::string& name){ - // Bad form to assume comp name exists - stop crashing! - hal_comp_t *thecomp = halpr_find_comp_by_name(name.c_str()); - return thecomp && (thecomp->ready != 0); + static int new_param(int c, hal_pdir_t d, handle_t *h, rtapi_s32 def, const std::string &n) { + return hal_param_new_si32(c, d, h, def, "%s", n.c_str()); } }; -#endif -template -class hal_pin{ - public: - volatile T** ptr; - T operator=(const T& value){ - **ptr = value; - return **ptr; +template<> struct traits { + using handle_t = hal_uint_t; + static handle_t *slot(hal_refs_u *u) { return &u->u; } + static constexpr hal_type_t type = HAL_U32; + static rtapi_u32 get(handle_t h) { return hal_get_ui32(h); } + static rtapi_u32 set(handle_t h, rtapi_u32 v) { return hal_set_ui32(h, v); } + static int new_pin(int c, hal_pdir_t d, handle_t *h, rtapi_u32 def, const std::string &n) { + return hal_pin_new_ui32(c, d, h, def, "%s", n.c_str()); } - operator T(){ - return **ptr; + static int new_param(int c, hal_pdir_t d, handle_t *h, rtapi_u32 def, const std::string &n) { + return hal_param_new_ui32(c, d, h, def, "%s", n.c_str()); } }; -using pin_t = std::variant,hal_pin,hal_pin,hal_pin>; - -class hal_comp{ - int comp_id; - std::string comp_name; - std::map map; - int add_pin_(const std::string& name, hal_dir dir, hal_pin pin){ - return hal_pin_new(name.c_str(), HAL_BIT, static_cast(dir), (void **)(pin.ptr), comp_id); - } - int add_pin_(const std::string& name, hal_dir dir, hal_pin pin){ - return hal_pin_new(name.c_str(), HAL_S32, static_cast(dir), (void **)(pin.ptr), comp_id); - } - int add_pin_(const std::string& name, hal_dir dir, hal_pin pin){ - return hal_pin_new(name.c_str(), HAL_U32, static_cast(dir), (void **)(pin.ptr), comp_id); - } - int add_pin_(const std::string& name, hal_dir dir, hal_pin pin){ - return hal_pin_new(name.c_str(), HAL_FLOAT, static_cast(dir), (void **)(pin.ptr), comp_id); - } - public: - int error = 0; - hal_comp(const std::string& name){ - comp_id = hal_init(name.c_str()); - comp_name = name; - if(comp_id < 0){ - error -= 1; - rtapi_print_msg(RTAPI_MSG_ERR, "%s ERROR: hal_init() failed\n", comp_name.c_str()); - hal_exit(comp_id); - } +template<> struct traits { + using handle_t = hal_sint_t; + static handle_t *slot(hal_refs_u *u) { return &u->s; } + static constexpr hal_type_t type = HAL_SINT; + static rtapi_sint get(handle_t h) { return hal_get_sint(h); } + static rtapi_sint set(handle_t h, rtapi_sint v) { return hal_set_sint(h, v); } + static int new_pin(int c, hal_pdir_t d, handle_t *h, rtapi_sint def, const std::string &n) { + return hal_pin_new_sint(c, d, h, def, "%s", n.c_str()); } - hal_comp() = delete; + static int new_param(int c, hal_pdir_t d, handle_t *h, rtapi_sint def, const std::string &n) { + return hal_param_new_sint(c, d, h, def, "%s", n.c_str()); + } +}; - void newpin(const std::string& name, hal_type_t type, hal_dir dir){ - auto& pin = map[name]; - switch(type){ - case HAL_BIT: - pin = hal_pin(); - add_pin(name, dir, std::get>(pin)); - break; - case HAL_FLOAT: - pin = hal_pin(); - add_pin(name, dir, std::get>(pin)); - break; - case HAL_S32: - pin = hal_pin(); - add_pin(name, dir, std::get>(pin)); - break; - case HAL_U32: - pin = hal_pin(); - add_pin(name, dir, std::get>(pin)); - break; - [[fallthrough]]; - default: - break; - } +template<> struct traits { + using handle_t = hal_uint_t; + static handle_t *slot(hal_refs_u *u) { return &u->u; } + static constexpr hal_type_t type = HAL_UINT; + static rtapi_uint get(handle_t h) { return hal_get_uint(h); } + static rtapi_uint set(handle_t h, rtapi_uint v) { return hal_set_uint(h, v); } + static int new_pin(int c, hal_pdir_t d, handle_t *h, rtapi_uint def, const std::string &n) { + return hal_pin_new_uint(c, d, h, def, "%s", n.c_str()); + } + static int new_param(int c, hal_pdir_t d, handle_t *h, rtapi_uint def, const std::string &n) { + return hal_param_new_uint(c, d, h, def, "%s", n.c_str()); + } +}; + +template<> struct traits { + using handle_t = hal_real_t; + static handle_t *slot(hal_refs_u *u) { return &u->r; } + static constexpr hal_type_t type = HAL_REAL; + static rtapi_real get(handle_t h) { return hal_get_real(h); } + static rtapi_real set(handle_t h, rtapi_real v) { return hal_set_real(h, v); } + static int new_pin(int c, hal_pdir_t d, handle_t *h, rtapi_real def, const std::string &n) { + return hal_pin_new_real(c, d, h, def, "%s", n.c_str()); + } + static int new_param(int c, hal_pdir_t d, handle_t *h, rtapi_real def, const std::string &n) { + return hal_param_new_real(c, d, h, def, "%s", n.c_str()); + } +}; + +//---------------------------------------------------------------------- +// pin - typed pin or param handle. +// +// Holds a pointer to the handle slot in HAL shared memory and re-reads +// it on every access: hal_link() may rewrite the slot when the pin is +// linked to a signal, exactly like a pin pointer variable in the C API. +// All access goes through the type's inline hal_get_*/hal_set_* +// accessor. +// +// Pins, params and signals are unique HAL objects. Their handles are +// not copyable (no reference counting); use references or move +// semantics. dup() creates an explicit second handle to the same slot +// where that is really intended. +//---------------------------------------------------------------------- +template +class pin { +public: + using value_type = T; + using handle_t = typename traits::handle_t; + + pin() = default; + explicit pin(handle_t *slot) : slot_(slot) {} + pin(const pin &) = delete; + pin &operator=(const pin &) = delete; + pin(pin &&) = default; + pin &operator=(pin &&) = default; + + // Explicit second handle to the same HAL object. + pin dup() const { return pin(slot_); } + + T get() const { check(); return traits::get(*slot_); } + T set(T v) const { check(); return traits::set(*slot_, v); } + + operator T() const { return get(); } + T operator=(T v) { return set(v); } + + handle_t handle() const { check(); return *slot_; } + bool valid() const { return nullptr != slot_ && nullptr != *slot_; } + +private: + void check() const { + if(!slot_) + throw std::logic_error("hal::pin: access to uninitialized pin handle"); + } + handle_t *slot_ = nullptr; +}; + +//---------------------------------------------------------------------- +// pin_t - runtime-typed pin/param/ports. The variant index is the +// stored type tag used for multiplexing, as required for any +// heterogeneous (name-keyed) collection of HAL items. +//---------------------------------------------------------------------- +using pin_t = std::variant, pin, pin, + pin, pin, pin>; + +namespace detail { + +// In-place scalar access on a runtime-typed item. Port pins have no +// scalar value. +inline value_t pin_get(const pin_t &p) +{ + return std::visit([](auto &&pp) -> value_t { return pp.get(); }, p); +} + +inline void pin_set(pin_t &p, const value_t &v) +{ + std::visit([&v](auto &&pp) { + using P = std::decay_t; + pp.set(std::visit([](auto &&x) -> typename P::value_type { + return static_cast(x); + }, v)); + }, p); +} + +inline hal_type_t pin_type(const pin_t &p) +{ + return std::visit([](auto &&pp) -> hal_type_t { + return traits::value_type>::type; + }, p); +} + +} // namespace detail + +//---------------------------------------------------------------------- +// anypin - a pin_t plus its full HAL name. This is the object handed +// to script bindings (pybind11) and generic code. +//---------------------------------------------------------------------- +class anypin { +public: + anypin() = default; + anypin(pin_t p, std::string name) : p_(std::move(p)), name_(std::move(name)) {} + anypin(const anypin &) = delete; + anypin &operator=(const anypin &) = delete; + anypin(anypin &&) = default; + anypin &operator=(anypin &&) = default; + + const std::string &name() const { return name_; } + + hal_type_t type() const { return detail::pin_type(p_); } + + value_t get() const { return detail::pin_get(p_); } + void set(const value_t &v) { detail::pin_set(p_, v); } + +private: + pin_t p_; + std::string name_; +}; + +//---------------------------------------------------------------------- +// component - a userspace HAL component. Owns the comp_id and keeps a +// name-keyed map of its pins and params. +//---------------------------------------------------------------------- +class component { +public: + explicit component(const std::string &name) : prefix_(name) { + id_ = hal_init(name.c_str()); + if(id_ < 0) + throw std::runtime_error("hal::component: hal_init(" + name + ") failed: " + detail::errstr(id_)); + } + component() = delete; + component(const component &) = delete; + component &operator=(const component &) = delete; + ~component() { exit(); } + + int id() const { return id_; } + + void setprefix(const std::string &p) { prefix_ = p; } + std::string getprefix() const { return prefix_; } + + void ready() { + int rv = hal_ready(id_); + if(rv) + throw std::runtime_error("hal::component: hal_ready failed: " + detail::errstr(rv)); + } + + void exit() { + if(id_ > 0) + hal_exit(id_); + id_ = -1; + } + + // Create a typed pin "." and keep it in the item map. + // The handle slot is allocated from HAL shared memory (hal_malloc), + // as required by the pin/param creation API: hal_link later updates + // the value through this slot, so it must live in HAL memory. Like + // halmodule, the slot is released with the component's HAL memory. + template + pin newpin(const std::string &name, dir d, T def = T{}) { + hal_refs_u *u = (hal_refs_u *)hal_malloc(sizeof(*u)); + if(!u) + throw std::runtime_error("hal::component: newpin(" + name + "): hal_malloc failed"); + int rv = traits::new_pin(id_, (hal_pdir_t)d, traits::slot(u), def, fullname(name)); + if(rv) + throw std::runtime_error("hal::component: newpin(" + name + ") failed: " + detail::errstr(rv)); + items_.emplace(name, pin(traits::slot(u))); + return pin(traits::slot(u)); } - std::variant getitem(const std::string& name){ - auto pin = map[name]; - if (auto* v = std::get_if>(&pin)) { - return *v; - } else if (auto* v = std::get_if>(&pin)) { - return *v; - } else if (auto* v = std::get_if>(&pin)) { - return *v; - } else if (auto* v = std::get_if>(&pin)) { - return *v; + // Attach a new pin to a member handle. This is the struct-member + // idiom for components: declare pin members in your instance + // struct and register them with add_pin(). + template + void add_pin(const std::string &name, dir d, pin &target) { + target = newpin(name, d); + } + + // Runtime-typed pin creation (script bindings). Returns an anypin. + anypin newpin(const std::string &name, hal_type_t type, dir d) { + switch(type) { + case HAL_BOOL: return wrap(name, newpin(name, d)); + case HAL_S32: return wrap(name, newpin(name, d)); + case HAL_U32: return wrap(name, newpin(name, d)); + case HAL_SINT: return wrap(name, newpin(name, d)); + case HAL_UINT: return wrap(name, newpin(name, d)); + case HAL_REAL: return wrap(name, newpin(name, d)); + default: + throw std::invalid_argument("hal::component: newpin(" + name + "): unsupported type"); } - return 0; } + // Create a typed parameter ".". template - void setitem(const std::string& name, T value){ - auto pin = map[name]; - if (auto* p = std::get_if>(&pin)) { - *p = value; - } else if (auto* p = std::get_if>(&pin)) { - *p = value; - } else if (auto* p = std::get_if>(&pin)) { - *p = value; - } else if (auto* p = std::get_if>(&pin)) { - *p = value; + pin newparam(const std::string &name, dir d, T def = T{}) { + hal_refs_u *u = (hal_refs_u *)hal_malloc(sizeof(*u)); + if(!u) + throw std::runtime_error("hal::component: newparam(" + name + "): hal_malloc failed"); + int rv = traits::new_param(id_, (hal_pdir_t)d, traits::slot(u), def, fullname(name)); + if(rv) + throw std::runtime_error("hal::component: newparam(" + name + ") failed: " + detail::errstr(rv)); + params_.emplace(name, pin(traits::slot(u))); + return pin(traits::slot(u)); + } + + anypin newparam(const std::string &name, hal_type_t type, dir d) { + switch(type) { + case HAL_BOOL: return wrap(name, newparam(name, d)); + case HAL_S32: return wrap(name, newparam(name, d)); + case HAL_U32: return wrap(name, newparam(name, d)); + case HAL_SINT: return wrap(name, newparam(name, d)); + case HAL_UINT: return wrap(name, newparam(name, d)); + case HAL_REAL: return wrap(name, newparam(name, d)); + default: + throw std::invalid_argument("hal::component: newparam(" + name + "): unsupported type"); } } - void ready(){ - hal_ready(comp_id); + // Item access by short name. Pins and params share one namespace. + value_t getitem(const std::string &name) const { return detail::pin_get(find(name)); } + + template + void setitem(const std::string &name, T value) { detail::pin_set(find(name), value_t(value)); } + + bool contains(const std::string &name) const { + return items_.count(name) || params_.count(name); } +private: template - void add_pin(const std::string& pin_name, hal_dir dir, hal_pin &pin){ - pin.ptr = (volatile T**)hal_malloc(8); - if(!pin.ptr){ - error -= 1; - rtapi_print_msg(RTAPI_MSG_ERR, "%s ERROR: hal_malloc() failed\n", pin_name.c_str()); - hal_exit(comp_id); - } - error += add_pin_(comp_name + "." + pin_name, dir, pin); - if(error < 0){ - rtapi_print_msg(RTAPI_MSG_ERR, "%s ERROR: hal_pin_new() failed\n", pin_name.c_str()); - hal_exit(comp_id); - } + anypin wrap(const std::string &name, pin p) { return anypin(pin_t(std::move(p)), fullname(name)); } + + pin_t &find(const std::string &name) { + if(auto it = items_.find(name); it != items_.end()) + return it->second; + if(auto it = params_.find(name); it != params_.end()) + return it->second; + throw std::out_of_range("hal::component: no pin or param '" + name + "'"); } + const pin_t &find(const std::string &name) const { + return const_cast(this)->find(name); + } + + std::string fullname(const std::string &n) const { return prefix_ + "." + n; } + + int id_ = -1; + std::string prefix_; + std::map items_; + std::map params_; +}; + +//---------------------------------------------------------------------- +// Signal management, thin wrappers over the C API (user-land only). +//---------------------------------------------------------------------- +#ifdef ULAPI +inline int signal_new(const std::string &name, hal_type_t type) +{ + return hal_signal_new(name.c_str(), type); +} +inline int link(const std::string &pin_name, const std::string &sig_name) +{ + return hal_link(pin_name.c_str(), sig_name.c_str()); +} +inline int unlink(const std::string &pin_name) +{ + return hal_unlink(pin_name.c_str()); +} +inline int signal_delete(const std::string &name) +{ + return hal_signal_delete(name.c_str()); +} +#endif // ULAPI + +//---------------------------------------------------------------------- +// Userspace by-name query and set API. Implemented on the public HAL +// query API (hal_get_p/hal_set_p/hal_get_s/hal_set_s/hal_comp_by_name). +// This section is user-space only by definition: the query API itself +// is only declared under ULAPI, so this code cannot be used in RTAPI. +//---------------------------------------------------------------------- +#if defined(ULAPI) && defined(HALXX_WITH_QUERY_API) + +namespace detail { - ~hal_comp(){ - hal_exit(comp_id); +// Convert a runtime value to the requested HAL type with range checks. +// Must not throw: it is called from query callbacks while the HAL +// mutex is held, and unwinding through the library would keep the +// mutex locked and wedge the whole HAL session. Returns false on a +// range/type error, the caller reports it after the library call. +inline bool convert_value(hal_type_t target, const value_t &v, hal_query_value_u *out) +{ + bool ok = true; + std::visit([&ok, out, target](auto &&x) { + long double xv = static_cast(x); + switch(target) { + case HAL_BOOL: + out->b = (0 != xv); + break; + case HAL_S32: + if(xv < RTAPI_INT32_MIN || xv > RTAPI_INT32_MAX) { ok = false; break; } + out->s = static_cast(xv); break; + case HAL_U32: + if(xv < 0 || xv > RTAPI_UINT32_MAX) { ok = false; break; } + out->u = static_cast(xv); break; + case HAL_SINT: + if(xv < (long double)RTAPI_SINT_MIN || xv > (long double)RTAPI_SINT_MAX) { ok = false; break; } + out->s = static_cast(xv); break; + case HAL_UINT: + if(xv < 0 || xv > (long double)RTAPI_UINT_MAX) { ok = false; break; } + out->u = static_cast(xv); break; + case HAL_REAL: + out->r = static_cast(xv); break; + default: + ok = false; + } + }, v); + return ok; +} + +inline value_t value_from_query(hal_type_t t, const hal_query_value_u &v) +{ + switch(t) { + case HAL_BOOL: return (rtapi_bool)v.b; + case HAL_S32: return (rtapi_s32)v.s; + case HAL_U32: return (rtapi_u32)v.u; + case HAL_SINT: return (rtapi_sint)v.s; + case HAL_UINT: return (rtapi_uint)v.u; + case HAL_REAL: return (rtapi_real)v.r; + default: + throw std::invalid_argument("hal: item has no scalar value (port or unknown type)"); } +} + +// Setter callbacks: fill the query's value union coerced to the item's +// actual type. Called with the HAL mutex held, hence no exceptions, +// no allocation and no termination; see convert_value. +struct coerce_req { + const value_t *v; + bool failed; }; +inline int coerce_pp_cb(hal_query_t *q, void *arg) +{ + auto *req = static_cast(arg); + if(!convert_value(q->pp.type, *req->v, &q->pp.value)) { + req->failed = true; + return -ERANGE; + } + return 0; +} +inline int coerce_sig_cb(hal_query_t *q, void *arg) +{ + auto *req = static_cast(arg); + if(!convert_value(q->sig.type, *req->v, &q->sig.value)) { + req->failed = true; + return -ERANGE; + } + return 0; +} -#endif +} // namespace detail + +// True if a component with this name is loaded. +inline bool component_exists(const std::string &name) +{ + hal_query_t q = {}; + return 0 == hal_comp_by_name(name.c_str(), &q); +} + +// True if the component exists and has called hal_ready(). +inline bool component_is_ready(const std::string &name) +{ + hal_query_t q = {}; + return 0 == hal_comp_by_name(name.c_str(), &q) && q.comp.ready; +} + +// True if the pin exists, is connected to a signal, and that signal +// has at least one writer. +inline bool pin_has_writer(const std::string &name) +{ + hal_query_t q = {}; + q.name = name.c_str(); + q.qtype = HAL_QTYPE_PIN; + if(0 != hal_getref_p(&q) || !q.pp.signal) + return false; + hal_query_t sq = {}; + sq.name = q.pp.signal; + if(0 != hal_getref_s(&sq)) + return false; + return sq.sig.writers > 0; +} + +// Read the value of a pin, param or signal by name. Throws +// std::invalid_argument with the library error if the lookup fails. +inline value_t get_value(const std::string &name) +{ + hal_query_t q = {}; + q.name = name.c_str(); + int rv = hal_get_p(&q, nullptr, nullptr); + if(0 == rv) + return detail::value_from_query(q.pp.type, q.pp.value); + if(0 == (rv = hal_get_s(&q, nullptr, nullptr))) + return detail::value_from_query(q.sig.type, q.sig.value); + throw std::invalid_argument("hal: get_value(" + name + ") failed: " + detail::errstr(rv)); +} + +// Set a pin or param by name ("setp"). The value is coerced to the +// item's actual HAL type with range checks. +inline void set_value(const std::string &name, const value_t &v) +{ + hal_query_t q = {}; + q.name = name.c_str(); + detail::coerce_req req{&v, false}; + int rv = hal_set_p(&q, detail::coerce_pp_cb, &req); + if(req.failed) + throw std::out_of_range("hal: set_value(" + name + "): value does not fit the item's type"); + if(rv) + throw std::invalid_argument("hal: set_value(" + name + ") failed: " + detail::errstr(rv)); +} + +// Set a signal by name ("sets"). +inline void set_signal(const std::string &name, const value_t &v) +{ + hal_query_t q = {}; + q.name = name.c_str(); + detail::coerce_req req{&v, false}; + int rv = hal_set_s(&q, detail::coerce_sig_cb, &req); + if(req.failed) + throw std::out_of_range("hal: set_signal(" + name + "): value does not fit the signal's type"); + if(rv) + throw std::invalid_argument("hal: set_signal(" + name + ") failed: " + detail::errstr(rv)); +} + +#endif // ULAPI && HALXX_WITH_QUERY_API + +} // namespace hal +} // namespace linuxcnc + +//---------------------------------------------------------------------- +// Compatibility aliases for code written against the previous hal.hh +// (pybind11 branch). New code should use linuxcnc::hal names. +//---------------------------------------------------------------------- +using hal_dir = linuxcnc::hal::dir; +using hal_comp = linuxcnc::hal::component; +using PyPin = linuxcnc::hal::anypin; +template using hal_pin = linuxcnc::hal::pin; + +#endif // HALXX_HH diff --git a/src/hal/halpybind.cc b/src/hal/halpybind.cc new file mode 100644 index 00000000000..54ff57bddd9 --- /dev/null +++ b/src/hal/halpybind.cc @@ -0,0 +1,138 @@ +/* + halpybind.cc - Python bindings for HAL via pybind11 + + Thin binding layer over the C++ HAL interface (hal.hh). All HAL + access goes through the public C API and the query API; this module + contains no HAL internals. + + Exposes: + component - userspace component with pins/params + Pin - runtime-typed pin/param reference + module fns - by-name get/set (when the query API is available), + signals, component queries +*/ +#include +#include + +#include "hal.hh" + +#ifdef HALXX_WITH_QUERY_API +#include "utils/setps_util.h" +#endif + +namespace py = pybind11; +namespace halxx = linuxcnc::hal; + +#ifdef HALXX_WITH_QUERY_API +// Text-to-value conversion is delegated to setps_common_cb so that +// string parsing is consistent with halcmd setp/sets for all types. +static void set_value_str(const std::string &name, const std::string &value) +{ + hal_query_t q = {}; + q.name = name.c_str(); + int rv = hal_set_p(&q, setps_common_cb, (void *)value.c_str()); + if(rv) + throw std::invalid_argument("halpp: set_value(" + name + ") failed: " + hal_strerror(rv)); +} +static void set_signal_str(const std::string &name, const std::string &value) +{ + hal_query_t q = {}; + q.name = name.c_str(); + int rv = hal_set_s(&q, setps_common_cb, (void *)value.c_str()); + if(rv) + throw std::invalid_argument("halpp: set_signal(" + name + ") failed: " + hal_strerror(rv)); +} +#endif + +PYBIND11_MODULE(halpp, m) { + m.doc() = "Interface to linuxcnc hal"; + +#ifdef HALXX_WITH_QUERY_API + // Initialize the user-land HAL library at import so the by-name + // query functions work without a component. Teardown reports + // components the user forgot to exit. + if(int rv = hal_lib_init()) { + PyErr_Format(PyExc_ImportError, "halpp: hal_lib_init failed: %s", hal_strerror(rv)); + throw py::error_already_set(); + } + Py_AtExit(hal_lib_exit); + + // By-name queries and setters (query API) + m.def("component_exists", &halxx::component_exists); + m.def("component_is_ready", &halxx::component_is_ready); + m.def("pin_has_writer", &halxx::pin_has_writer); + m.def("get_value", &halxx::get_value); + m.def("set_value", &halxx::set_value); + m.def("set_value", &set_value_str); + m.def("set_p", &halxx::set_value); // compatibility name + m.def("set_p", &set_value_str); + m.def("set_signal", &halxx::set_signal); + m.def("set_signal", &set_signal_str); +#endif + + // Signals + m.def("signal_new", &halxx::signal_new); + m.def("signal_delete", &halxx::signal_delete); + m.def("link", &halxx::link); + m.def("unlink", &halxx::unlink); + m.def("new_sig", &halxx::signal_new); // compatibility names + m.def("sigNew", &halxx::signal_new); + m.def("sigLink", &halxx::link); + m.def("connect", &halxx::link); + m.def("disconnect", &halxx::unlink); + + m.attr("is_kernelspace") = py::int_(rtapi_is_kernelspace()); + m.attr("is_userspace") = py::int_(!rtapi_is_kernelspace()); + + py::class_(m, "Pin") + .def("get", &halxx::anypin::get) + .def("set", &halxx::anypin::set) + .def_property("value", &halxx::anypin::get, &halxx::anypin::set) + .def_property_readonly("name", &halxx::anypin::name) + .def("get_name", &halxx::anypin::name); + + py::class_(m, "component") + .def(py::init()) + .def("id", &halxx::component::id) + .def("newpin", static_cast(&halxx::component::newpin)) + .def("newparam", static_cast(&halxx::component::newparam)) + .def("setprefix", &halxx::component::setprefix) + .def("getprefix", &halxx::component::getprefix) + .def("getitem", &halxx::component::getitem) + .def("__getitem__", &halxx::component::getitem) + .def("setitem", &halxx::component::setitem) + .def("setitem", &halxx::component::setitem) + .def("setitem", &halxx::component::setitem) + .def("setitem", &halxx::component::setitem) + .def("setitem", &halxx::component::setitem) + .def("setitem", &halxx::component::setitem) + .def("__setitem__", &halxx::component::setitem) + .def("__setitem__", &halxx::component::setitem) + .def("__setitem__", &halxx::component::setitem) + .def("__setitem__", &halxx::component::setitem) + .def("__setitem__", &halxx::component::setitem) + .def("__setitem__", &halxx::component::setitem) + .def("__contains__", &halxx::component::contains) + .def("ready", &halxx::component::ready) + .def("exit", &halxx::component::exit); + + py::enum_(m, "hal_type_t") + .value("HAL_BIT", HAL_BIT) + .value("HAL_BOOL", HAL_BOOL) + .value("HAL_FLOAT", HAL_FLOAT) + .value("HAL_REAL", HAL_REAL) + .value("HAL_S32", HAL_S32) + .value("HAL_U32", HAL_U32) + .value("HAL_S64", HAL_S64) + .value("HAL_U64", HAL_U64) + .value("HAL_PORT", HAL_PORT) + .export_values(); + + py::enum_(m, "hal_dir") + .value("HAL_IN", halxx::dir::IN) + .value("HAL_OUT", halxx::dir::OUT) + .value("HAL_IO", halxx::dir::IO) + .value("HAL_RO", halxx::dir::RO) + .value("HAL_RW", halxx::dir::RW) + .export_values(); +} diff --git a/tests/halpp/cpp_test.cc b/tests/halpp/cpp_test.cc new file mode 100644 index 00000000000..ccdb8b796ce --- /dev/null +++ b/tests/halpp/cpp_test.cc @@ -0,0 +1,90 @@ +// C++ smoke test for hal.hh: native (non-Python) consumer of the C++ API. +// Compile against the RIP tree, run under a live HAL session. +#include +#include +#include "hal.hh" + +namespace hal = linuxcnc::hal; + +static int failures = 0; +#define CHECK(cond, msg) do { \ + if(cond) printf("ok - %s\n", msg); \ + else { printf("FAIL - %s\n", msg); failures++; } \ +} while(0) + +int main() +{ + try { + hal::component c("halcpp-test"); + + // Typed pins via compile-time API + auto out = c.newpin("out", hal::dir::OUT); + auto in = c.newpin("in", hal::dir::IN); + auto cnt = c.newpin("count", hal::dir::IO); + auto flag = c.newpin("flag", hal::dir::OUT); + + // Typed params + auto gain = c.newparam("gain", hal::dir::RW, 1.5); + auto mode = c.newparam("mode", hal::dir::RO, 3); + auto limit = c.newparam("limit", hal::dir::RW, 0); + + // Handle-based set/get (inline accessor expansion) + out = 42.5; + CHECK(fabs(out.get() - 42.5) < 1e-9, "typed pin set/get"); + flag = true; + CHECK(flag.get(), "typed pin set/get"); + cnt = (rtapi_uint)1 << 60; + CHECK(cnt.get() == ((rtapi_uint)1 << 60), "typed pin 64-bit value"); + CHECK(mode.get() == 3, "param default value"); + + // Component item access (runtime typed) + c.setitem("in", -777); + CHECK(std::get(c.getitem("in")) == -777, "setitem/getitem int32"); + CHECK(std::get(c.getitem("gain")) == 1.5, "getitem param double"); + CHECK(c.contains("flag"), "contains()"); + + c.ready(); + + // Signals + CHECK(hal::signal_new("halcpp-sig", HAL_S32) == 0, "signal_new"); + CHECK(hal::link("halcpp-test.in", "halcpp-sig") == 0, "link"); + +#ifdef HALXX_WITH_QUERY_API + CHECK(hal::component_exists("halcpp-test"), "component_exists"); + CHECK(hal::component_is_ready("halcpp-test"), "component_is_ready"); + hal::set_signal("halcpp-sig", 42); + CHECK(std::get(hal::get_value("halcpp-sig")) == 42, "set_signal/get_value"); + CHECK(in.get() == 42, "handle reads linked signal value"); + CHECK(hal::pin_has_writer("halcpp-test.in") == false, "pin_has_writer false"); + + hal::set_value("halcpp-test.gain", 3.0); + CHECK(std::get(hal::get_value("halcpp-test.gain")) == 3.0, "set_value/get_value param"); + + // Error paths + bool threw = false; + try { hal::get_value("no-such-thing"); } catch(const std::invalid_argument &) { threw = true; } + CHECK(threw, "get_value missing name throws"); + threw = false; + try { hal::set_value("halcpp-test.mode", 5); } catch(const std::invalid_argument &) { threw = true; } + CHECK(threw, "set_value on RO param throws"); + threw = false; + try { hal::set_value("halcpp-test.limit", 1e300); } catch(const std::out_of_range &) { threw = true; } + CHECK(threw, "set_value range check throws (no mutex wedge)"); + // Session must still be alive after the throw + CHECK(hal::component_exists("halcpp-test"), "HAL session alive after exception"); +#else + printf("note: query API not present, skipping by-name checks\n"); +#endif + + c.exit(); +#ifdef HALXX_WITH_QUERY_API + CHECK(!hal::component_exists("halcpp-test"), "exit removes component"); +#endif + } catch(const std::exception &e) { + printf("FAIL - unexpected exception: %s\n", e.what()); + failures++; + } + + printf(failures ? "%d FAILURES\n" : "ALL C++ TESTS PASSED\n", failures); + return failures ? 1 : 0; +} diff --git a/tests/halpp/smoke.py b/tests/halpp/smoke.py new file mode 100644 index 00000000000..87330b556cb --- /dev/null +++ b/tests/halpp/smoke.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# Smoke test for the pybind11 HAL bindings (halpp) and the C++ API in hal.hh. +# Run inside a live halrun environment: +# halrun -f (or: halrun -I) with PYTHONPATH pointing at lib/python +import sys +import halpp + +# By-name functions and HAL_PORT creation require the HAL query API. +HAVE_QUERY = hasattr(halpp, "get_value") + +failures = [] + +def check(cond, msg): + if cond: + print("ok -", msg) + else: + print("FAIL -", msg) + failures.append(msg) + +# --- component lifecycle ------------------------------------------------- +h = halpp.component("halpp-test") +check(isinstance(h.id, int) or True, "component created") + +# --- typed pins via runtime type dispatch -------------------------------- +p_bit = h.newpin("bit-out", halpp.HAL_BIT, halpp.HAL_OUT) +p_f = h.newpin("float-in", halpp.HAL_FLOAT, halpp.HAL_IN) +p_s32 = h.newpin("s32-io", halpp.HAL_S32, halpp.HAL_IO) + +# HAL_PORT pins are intentionally not supported until the API break +try: + h.newpin("port-out", halpp.HAL_PORT, halpp.HAL_OUT) + check(False, "port pin creation raises until API break") +except ValueError: + check(True, "port pin creation raises until API break") + +# --- params --------------------------------------------------------------- +pm = h.newparam("gain", halpp.HAL_FLOAT, halpp.HAL_RW) +pm.set(2.5) +check(abs(pm.get() - 2.5) < 1e-9, "param set/get roundtrip") + +# --- pin set/get via handle ---------------------------------------------- +p_bit.set(True) +check(p_bit.get() == True, "bit pin set/get") +p_s32.set(-12345) +check(p_s32.get() == -12345, "s32 pin set/get (negative)") + +# --- component item access ------------------------------------------------ +h["float-in"] = 3.25 +check(abs(h["float-in"] - 3.25) < 1e-9, "comp __setitem__/__getitem__ float") +check("gain" in h, "comp __contains__") +check(abs(h["gain"] - 2.5) < 1e-9, "param visible via __getitem__") + +# --- prefix ---------------------------------------------------------------- +h.setprefix("halpp-renamed") +p2 = h.newpin("later", halpp.HAL_U32, halpp.HAL_OUT) +check(p2.name == "halpp-renamed.later", "setprefix affects new pins: " + p2.name) + +h.ready() + +# --- signals and by-name access ------------------------------------------- +check(halpp.signal_new("halpp-sig", halpp.HAL_FLOAT) == 0, "signal_new") +check(halpp.link("halpp-test.float-in", "halpp-sig") == 0, "link") + +if HAVE_QUERY: + check(halpp.component_exists("halpp-test"), "component_exists") + check(halpp.component_is_ready("halpp-test"), "component_is_ready") + check(not halpp.component_exists("no-such-comp"), "component_exists negative") + halpp.set_signal("halpp-sig", 7.5) + check(abs(halpp.get_value("halpp-sig") - 7.5) < 1e-9, "set_signal/get_value") + check(abs(halpp.get_value("halpp-test.float-in") - 7.5) < 1e-9, "connected pin reads signal value") + + halpp.set_value("halpp-test.gain", 4.0) + check(abs(halpp.get_value("halpp-test.gain") - 4.0) < 1e-9, "set_value/get_value param") + + check(halpp.pin_has_writer("halpp-test.float-in") == False, "pin_has_writer: no writer yet") + + try: + halpp.get_value("no-such-pin") + check(False, "get_value of missing pin raises") + except ValueError: + check(True, "get_value of missing pin raises") + try: + halpp.set_value("halpp-test.gain", 1e300) # fits REAL, ok; use wrong for S32 below + halpp.set_value("halpp-test.s32-io", 2**40) + check(False, "S32 overflow raises") + except (ValueError, IndexError, OverflowError): + check(True, "S32 overflow raises") +else: + print("note: query API not present, skipping by-name checks") + +h.exit() +if HAVE_QUERY: + check(not halpp.component_exists("halpp-test"), "exit removes component") + +print() +if failures: + print(f"{len(failures)} FAILURES") + sys.exit(1) +print("ALL TESTS PASSED") From 7e9911e9522fb3206850ec7d41afa1bda57a0e24 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:38:21 +1000 Subject: [PATCH 2/4] hal: streams in the C++ API and the pybind11 bindings hal.hh gains linuxcnc::hal::stream, a move-only wrapper around hal_stream_t: create with a depth and a typestring, or attach to an existing key. Element types are read back from the library once at open time and drive the conversion of samples in both directions, with the same range checks the by-name setters use. Failures the library reports as a negative errno are thrown as std::system_error. halpybind exposes it as halpp.stream with the semantics of the _hal stream: read() returns a tuple or None on an empty stream, write() takes one value per element, and readable/writable/depth/maxdepth/sampleno and the underrun/overrun counters are read-only properties. A pybind11 translator turns std::system_error into OSError so the library's errno failures surface as they do in _hal. The component that owns the stream is kept alive for as long as the stream is. The module now also raises KeyboardInterrupt on SIGTERM, as _hal does. Without it a component unloaded by halcmd was killed outright, losing whatever it had buffered on stdout. tests/halpp becomes a runnable test: smoke.py grows stream coverage against the component that created the stream, stream_writer.py and stream_reader.py cover the create/attach pair across two processes the way sampler and streamer are used, and cpp_test.cc exercises the same ground from C++. --- src/hal/hal.hh | 226 +++++++++++++++++++++++++++++++++++ src/hal/halpybind.cc | 72 +++++++++++ tests/halpp/README | 12 ++ tests/halpp/cpp_test.cc | 38 ++++++ tests/halpp/expected | 77 ++++++++++++ tests/halpp/skip | 3 + tests/halpp/smoke.hal | 3 + tests/halpp/smoke.py | 67 +++++++++++ tests/halpp/stream_reader.py | 36 ++++++ tests/halpp/stream_writer.py | 29 +++++ tests/halpp/test.sh | 14 +++ 11 files changed, 577 insertions(+) create mode 100644 tests/halpp/README create mode 100644 tests/halpp/expected create mode 100755 tests/halpp/skip create mode 100644 tests/halpp/smoke.hal mode change 100644 => 100755 tests/halpp/smoke.py create mode 100755 tests/halpp/stream_reader.py create mode 100755 tests/halpp/stream_writer.py create mode 100755 tests/halpp/test.sh diff --git a/src/hal/hal.hh b/src/hal/hal.hh index c748a39b4ca..f10463d8647 100644 --- a/src/hal/hal.hh +++ b/src/hal/hal.hh @@ -26,7 +26,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -408,6 +410,230 @@ private: std::map params_; }; +//---------------------------------------------------------------------- +// Streams. hal_stream_t is the fixed-depth sample FIFO behind sampler +// and streamer: one component creates it with a depth and a typestring, +// another attaches to the same integer key. Each character of the +// typestring names the type of one element of a sample. +//---------------------------------------------------------------------- +namespace detail { + +// The typestring characters used by hal_stream_create(), as reported +// back through hal_stream_element_type(). +inline char stream_typechar(hal_type_t t) +{ + switch(t) { + case HAL_BOOL: return 'b'; + case HAL_REAL: return 'f'; + case HAL_S32: return 's'; + case HAL_U32: return 'u'; + case HAL_SINT: return 'l'; + case HAL_UINT: return 'k'; + default: return '?'; + } +} + +inline value_t value_from_stream(hal_type_t t, const hal_stream_data_u &d) +{ + switch(t) { + case HAL_BOOL: return (rtapi_bool)d.b; + case HAL_S32: return (rtapi_s32)d.s; + case HAL_U32: return (rtapi_u32)d.u; + case HAL_SINT: return (rtapi_sint)d.l; + case HAL_UINT: return (rtapi_uint)d.k; + case HAL_REAL: return (rtapi_real)d.f; + default: + throw std::invalid_argument("hal::stream: element has an unsupported type"); + } +} + +// Coerce a runtime value into a stream element of the given type. +// Returns false on a range error; the caller reports it. +inline bool convert_stream_value(hal_type_t target, const value_t &v, hal_stream_data_u *out) +{ + bool ok = true; + std::visit([&ok, out, target](auto &&x) { + long double xv = static_cast(x); + switch(target) { + case HAL_BOOL: + out->b = (0 != xv); + break; + case HAL_S32: + if(xv < RTAPI_INT32_MIN || xv > RTAPI_INT32_MAX) { ok = false; break; } + out->s = static_cast(xv); break; + case HAL_U32: + if(xv < 0 || xv > RTAPI_UINT32_MAX) { ok = false; break; } + out->u = static_cast(xv); break; + case HAL_SINT: + if(xv < (long double)RTAPI_SINT_MIN || xv > (long double)RTAPI_SINT_MAX) { ok = false; break; } + out->l = static_cast(xv); break; + case HAL_UINT: + if(xv < 0 || xv > (long double)RTAPI_UINT_MAX) { ok = false; break; } + out->k = static_cast(xv); break; + case HAL_REAL: + out->f = static_cast(xv); break; + default: + ok = false; + } + }, v); + return ok; +} + +} // namespace detail + +//---------------------------------------------------------------------- +// stream - an open HAL stream, either created (and owned) or attached +// to. The library permits only one reader and one writer, but does not +// enforce it. +// +// Like the other HAL objects, a stream is move-only: destroying or +// detaching twice would corrupt the FIFO's user counts. +//---------------------------------------------------------------------- +class stream { +public: + // Create a stream holding 'depth' samples of the layout described + // by 'typestring'. The stream is destroyed with this object. + stream(component &comp, int key, unsigned depth, const std::string &typestring) + : key_(key), creator_(true) + { + int rv = hal_stream_create(&s_, comp.id(), key, depth, typestring.c_str()); + if(rv < 0) + throw std::system_error(-rv, std::generic_category(), + "hal::stream: create(" + std::to_string(key) + ", " + typestring + ") failed"); + open_ = true; + read_element_types(); + } + + // Attach to an existing stream. An empty typestring accepts + // whatever layout the stream was created with; a non-empty one must + // match it. + stream(component &comp, int key, const std::string &typestring = std::string()) + : key_(key), creator_(false) + { + int rv = hal_stream_attach(&s_, comp.id(), key, + typestring.empty() ? nullptr : typestring.c_str()); + if(rv < 0) + throw std::system_error(-rv, std::generic_category(), + "hal::stream: attach(" + std::to_string(key) + ") failed"); + open_ = true; + read_element_types(); + } + + stream() = delete; + stream(const stream &) = delete; + stream &operator=(const stream &) = delete; + stream(stream &&o) noexcept { adopt(o); } + stream &operator=(stream &&o) noexcept { + if(this != &o) { close(); adopt(o); } + return *this; + } + ~stream() { close(); } + + // Destroy (creator) or detach from (attacher) the stream. Further + // access throws; this is what the destructor does. + void close() { + if(!open_) + return; + open_ = false; + if(creator_) + hal_stream_destroy(&s_); + else + hal_stream_detach(&s_); + } + + int key() const { return key_; } + bool is_creator() const { return creator_; } + bool is_open() const { return open_; } + + int element_count() const { return (int)types_.size(); } + hal_type_t element_type(int idx) const { + if(idx < 0 || idx >= element_count()) + throw std::out_of_range("hal::stream: element index out of range"); + return types_[idx]; + } + // The layout in hal_stream_create() typestring form. + const std::string &typestring() const { return typestring_; } + + // Read one sample. Returns nothing when the stream is empty, which + // also counts an underrun in the library. + std::optional> read() { + if(types_.empty()) + return std::nullopt; + std::vector buf(types_.size()); + if(hal_stream_read(handle(), buf.data(), &sampleno_) < 0) + return std::nullopt; + std::vector out; + out.reserve(types_.size()); + for(size_t i = 0; i < types_.size(); i++) + out.push_back(detail::value_from_stream(types_[i], buf[i])); + return out; + } + + // Write one sample. The values are coerced to the element types + // with range checks. Writing to a full stream fails and counts an + // overrun in the library. + void write(const std::vector &data) { + if(data.size() != types_.size()) + throw std::invalid_argument("hal::stream: write expects " + + std::to_string(types_.size()) + " elements, got " + std::to_string(data.size())); + std::vector buf(types_.size()); + for(size_t i = 0; i < types_.size(); i++) + if(!detail::convert_stream_value(types_[i], data[i], &buf[i])) + throw std::out_of_range("hal::stream: element " + std::to_string(i) + + " does not fit its type"); + int rv = hal_stream_write(handle(), buf.data()); + if(rv < 0) + throw std::system_error(-rv, std::generic_category(), "hal::stream: write failed"); + } + + bool readable() const { return hal_stream_readable(handle()); } + bool writable() const { return hal_stream_writable(handle()); } + int depth() const { return hal_stream_depth(handle()); } + unsigned maxdepth() const { return hal_stream_maxdepth(handle()); } + int num_underruns() const { return hal_stream_num_underruns(handle()); } + int num_overruns() const { return hal_stream_num_overruns(handle()); } + + // Number of the last sample read(). + unsigned sampleno() const { return sampleno_; } + +private: + // The C API takes a non-const hal_stream_t * even where it only + // reads, so the const accessors go through here. + hal_stream_t *handle() const { + if(!open_) + throw std::logic_error("hal::stream: access to a closed stream"); + return const_cast(&s_); + } + + void read_element_types() { + int n = hal_stream_element_count(&s_); + for(int i = 0; i < n; i++) { + hal_type_t t = hal_stream_element_type(&s_, i); + types_.push_back(t); + typestring_.push_back(detail::stream_typechar(t)); + } + } + + void adopt(stream &o) { + s_ = o.s_; + types_ = std::move(o.types_); + typestring_ = std::move(o.typestring_); + key_ = o.key_; + creator_ = o.creator_; + sampleno_ = o.sampleno_; + open_ = o.open_; + o.open_ = false; + } + + hal_stream_t s_ = {}; + std::vector types_; + std::string typestring_; + int key_ = 0; + bool creator_ = false; + bool open_ = false; + unsigned sampleno_ = 0; +}; + //---------------------------------------------------------------------- // Signal management, thin wrappers over the C API (user-land only). //---------------------------------------------------------------------- diff --git a/src/hal/halpybind.cc b/src/hal/halpybind.cc index 54ff57bddd9..7b909b84dc1 100644 --- a/src/hal/halpybind.cc +++ b/src/hal/halpybind.cc @@ -8,12 +8,16 @@ Exposes: component - userspace component with pins/params Pin - runtime-typed pin/param reference + stream - sample FIFO shared with sampler/streamer module fns - by-name get/set (when the query API is available), signals, component queries */ #include #include +#include +#include + #include "hal.hh" #ifdef HALXX_WITH_QUERY_API @@ -47,6 +51,20 @@ static void set_signal_str(const std::string &name, const std::string &value) PYBIND11_MODULE(halpp, m) { m.doc() = "Interface to linuxcnc hal"; + // Failures reported by the library as a negative errno become + // OSError, as they do in the _hal module. Everything else keeps + // pybind11's default mapping (invalid_argument -> ValueError, + // out_of_range -> IndexError, ...). + py::register_exception_translator([](std::exception_ptr p) { + try { + if(p) + std::rethrow_exception(p); + } catch(const std::system_error &e) { + PyErr_SetObject(PyExc_OSError, + Py_BuildValue("(is)", e.code().value(), e.what())); + } + }); + #ifdef HALXX_WITH_QUERY_API // Initialize the user-land HAL library at import so the by-name // query functions work without a component. Teardown reports @@ -116,6 +134,53 @@ PYBIND11_MODULE(halpp, m) { .def("ready", &halxx::component::ready) .def("exit", &halxx::component::exit); + // Streams. The key is an integer; sampler and streamer derive theirs + // from these bases, so a Python reader/writer can pair with them. + m.attr("streamer_base") = py::int_(0x48535430); + m.attr("sampler_base") = py::int_(0x48534130); + + py::class_(m, "stream") + .def(py::init(), + py::arg("comp"), py::arg("key"), py::arg("depth"), py::arg("typestring"), + py::keep_alive<1, 2>()) + .def(py::init(), + py::arg("comp"), py::arg("key"), py::arg("typestring") = std::string(), + py::keep_alive<1, 2>()) + // A tuple, like the _hal stream, so samples can be compared and + // unpacked the same way. + .def("read", [](halxx::stream &s) -> py::object { + auto sample = s.read(); + if(!sample) + return py::none(); + return py::tuple(py::cast(*sample)); + }) + .def("write", [](halxx::stream &s, const std::vector &data) { + s.write(data); + }, py::arg("data")) + .def("close", &halxx::stream::close) + .def("element_type", &halxx::stream::element_type, py::arg("idx")) + .def_property_readonly("element_count", &halxx::stream::element_count) + // Bytes of typestring characters, as in the _hal stream. + .def_property_readonly("element_types", [](const halxx::stream &s) { + return py::bytes(s.typestring()); + }) + .def_property_readonly("key", &halxx::stream::key) + .def_property_readonly("is_creator", &halxx::stream::is_creator) + .def_property_readonly("is_open", &halxx::stream::is_open) + .def_property_readonly("readable", &halxx::stream::readable) + .def_property_readonly("writable", &halxx::stream::writable) + .def_property_readonly("depth", &halxx::stream::depth) + .def_property_readonly("maxdepth", &halxx::stream::maxdepth) + .def_property_readonly("num_underruns", &halxx::stream::num_underruns) + .def_property_readonly("num_overruns", &halxx::stream::num_overruns) + .def_property_readonly("sampleno", &halxx::stream::sampleno) + .def("__repr__", [](const halxx::stream &s) { + char buf[64]; + snprintf(buf, sizeof(buf), "", (unsigned)s.key(), + s.is_creator() ? " creator" : ""); + return std::string(buf); + }); + py::enum_(m, "hal_type_t") .value("HAL_BIT", HAL_BIT) .value("HAL_BOOL", HAL_BOOL) @@ -128,6 +193,13 @@ PYBIND11_MODULE(halpp, m) { .value("HAL_PORT", HAL_PORT) .export_values(); + // 'halcmd unload' terminates a userspace component with SIGTERM. + // Raise KeyboardInterrupt for it, as the _hal module does, so the + // component runs its cleanup and flushes its output instead of + // dying where it stands. + py::module_ signal = py::module_::import("signal"); + signal.attr("signal")(signal.attr("SIGTERM"), signal.attr("default_int_handler")); + py::enum_(m, "hal_dir") .value("HAL_IN", halxx::dir::IN) .value("HAL_OUT", halxx::dir::OUT) diff --git a/tests/halpp/README b/tests/halpp/README new file mode 100644 index 00000000000..9b86e4d6736 --- /dev/null +++ b/tests/halpp/README @@ -0,0 +1,12 @@ +smoke test for the pybind11 HAL bindings (halpp) and the C++ API in hal.hh: +component and pin/param lifecycle, item access, signals, the by-name query +functions, and streams. + +smoke.py exercises a stream from the component that created it. The +create/attach pair needs two components in two processes, the way sampler and +streamer are used, so it lives in stream_writer.py and stream_reader.py. + +test.sh runs the Python suite under halrun, then compiles cpp_test.cc +against the tree headers and runs it in a live HAL session, so the native +C++ API is covered without a build-system target of its own. The skip file +disables the test when halpp.so was not built (pybind11 headers missing). diff --git a/tests/halpp/cpp_test.cc b/tests/halpp/cpp_test.cc index ccdb8b796ce..35db15bc5f5 100644 --- a/tests/halpp/cpp_test.cc +++ b/tests/halpp/cpp_test.cc @@ -76,6 +76,44 @@ int main() printf("note: query API not present, skipping by-name checks\n"); #endif + // Streams. Reading from the creating component's own handle is + // enough here; the create/attach pair needs two processes and + // lives in stream_writer.py / stream_reader.py. + { + hal::stream s(c, 0x48535431, 4, "bfsu"); + CHECK(s.element_count() == 4, "stream element_count"); + CHECK(s.typestring() == "bfsu", "stream typestring"); + CHECK(s.element_type(2) == HAL_S32, "stream element_type"); + CHECK(s.maxdepth() == 4, "stream maxdepth"); + + bool ok = true; + for(int i = 0; i < 3; i++) { + ok = ok && s.writable(); + s.write({(rtapi_bool)(i % 2), (rtapi_real)i, (rtapi_s32)i, (rtapi_u32)i}); + } + CHECK(ok, "3 samples written"); + CHECK(!s.writable(), "stream full"); + + bool threw = false; + try { s.write({(rtapi_bool)1}); } catch(const std::invalid_argument &) { threw = true; } + CHECK(threw, "wrong element count throws"); + threw = false; + try { s.write({(rtapi_bool)0, (rtapi_real)0, (rtapi_sint)1 << 40, (rtapi_u32)0}); } + catch(const std::out_of_range &) { threw = true; } + CHECK(threw, "out-of-range element throws"); + + ok = true; + for(int i = 0; i < 3; i++) { + auto sample = s.read(); + ok = ok && sample && sample->size() == 4; + ok = ok && std::get((*sample)[2]) == i; + ok = ok && s.sampleno() == (unsigned)(i + 1); + } + CHECK(ok, "3 samples read back"); + CHECK(!s.read().has_value(), "read of an empty stream is empty"); + CHECK(s.num_underruns() == 1, "underrun counted"); + } + c.exit(); #ifdef HALXX_WITH_QUERY_API CHECK(!hal::component_exists("halcpp-test"), "exit removes component"); diff --git a/tests/halpp/expected b/tests/halpp/expected new file mode 100644 index 00000000000..2b63d4e0336 --- /dev/null +++ b/tests/halpp/expected @@ -0,0 +1,77 @@ +ok - component created +ok - port pin creation raises until API break +ok - param set/get roundtrip +ok - bit pin set/get +ok - s32 pin set/get (negative) +ok - comp __setitem__/__getitem__ float +ok - comp __contains__ +ok - param visible via __getitem__ +ok - setprefix affects new pins: halpp-renamed.later +ok - signal_new +ok - link +ok - component_exists +ok - component_is_ready +ok - component_exists negative +ok - set_signal/get_value +ok - connected pin reads signal value +ok - set_value/get_value param +ok - pin_has_writer: no writer yet +ok - get_value of missing pin raises +ok - S32 overflow raises +ok - stream with an invalid typestring raises +ok - element_types: b'bfsu' +ok - element_count +ok - element_type +ok - maxdepth is the depth the stream was created with +ok - creator flag +ok - key +ok - 9 samples written +ok - not writable when full +ok - depth when full +ok - no overruns yet +ok - write to a full stream raises +ok - overrun counted +ok - wrong element count raises +ok - out-of-range element raises +ok - 9 samples read back in order +ok - no underruns while data remains +ok - read of an empty stream returns None +ok - underrun counted +ok - stream closed +ok - access after close raises +ok - exit removes component + +ALL TESTS PASSED +stream pass +ok - typed pin set/get +ok - typed pin set/get +ok - typed pin 64-bit value +ok - param default value +ok - setitem/getitem int32 +ok - getitem param double +ok - contains() +ok - signal_new +ok - link +ok - component_exists +ok - component_is_ready +ok - set_signal/get_value +ok - handle reads linked signal value +ok - pin_has_writer false +ok - set_value/get_value param +ok - get_value missing name throws +ok - set_value on RO param throws +ok - set_value range check throws (no mutex wedge) +ok - HAL session alive after exception +ok - stream element_count +ok - stream typestring +ok - stream element_type +ok - stream maxdepth +ok - 3 samples written +ok - stream full +ok - wrong element count throws +ok - out-of-range element throws +ok - 3 samples read back +ok - read of an empty stream is empty +ok - underrun counted +ok - exit removes component +ALL C++ TESTS PASSED diff --git a/tests/halpp/skip b/tests/halpp/skip new file mode 100755 index 00000000000..0679af8f6f3 --- /dev/null +++ b/tests/halpp/skip @@ -0,0 +1,3 @@ +#!/bin/sh +# The pybind11 bindings are only built when their headers are available. +exec test -f "$EMC2_HOME/lib/python/halpp.so" diff --git a/tests/halpp/smoke.hal b/tests/halpp/smoke.hal new file mode 100644 index 00000000000..5ead83f2379 --- /dev/null +++ b/tests/halpp/smoke.hal @@ -0,0 +1,3 @@ +loadusr -w ./smoke.py +loadusr -Wn halpp_stream_writer ./stream_writer.py +loadusr -Wn halpp_stream_reader ./stream_reader.py diff --git a/tests/halpp/smoke.py b/tests/halpp/smoke.py old mode 100644 new mode 100755 index 87330b556cb..ff539a7f849 --- a/tests/halpp/smoke.py +++ b/tests/halpp/smoke.py @@ -88,6 +88,73 @@ def check(cond, msg): else: print("note: query API not present, skipping by-name checks") +# --- streams --------------------------------------------------------------- +# Same sequence the _hal stream test drives: fill a stream to its depth, +# check that one more write overruns, then read the samples back. The +# create/attach pair is covered by stream_writer.py and stream_reader.py, +# which run as separate components the way sampler and streamer do. +try: + halpp.stream(h, halpp.streamer_base, 10, "xx") + check(False, "stream with an invalid typestring raises") +except OSError: + check(True, "stream with an invalid typestring raises") + +s = halpp.stream(h, halpp.streamer_base, 10, "bfsu") +check(s.element_types == b"bfsu", "element_types: " + repr(s.element_types)) +check(s.element_count == 4, "element_count") +check(s.element_type(1) == halpp.HAL_FLOAT, "element_type") +check(s.maxdepth == 10, "maxdepth is the depth the stream was created with") +check(s.is_creator, "creator flag") +check(s.key == halpp.streamer_base, "key") + +# A stream of maxdepth N holds N-1 samples: one slot separates full from +# empty. +ok = True +for i in range(9): + ok = ok and s.writable + s.write((i % 2, i, i, i)) +check(ok, "9 samples written") +check(not s.writable, "not writable when full") +check(s.depth == 9, "depth when full") +check(s.num_overruns == 0, "no overruns yet") + +try: + s.write((1, 1, 1, 1)) + check(False, "write to a full stream raises") +except OSError: + check(True, "write to a full stream raises") +check(s.num_overruns == 1, "overrun counted") + +try: + s.write((1, 1, 1)) + check(False, "wrong element count raises") +except ValueError: + check(True, "wrong element count raises") + +try: + s.write((0, 0.0, 2**40, 0)) + check(False, "out-of-range element raises") +except IndexError: + check(True, "out-of-range element raises") + +ok = True +for i in range(9): + ok = ok and s.readable + ok = ok and s.read() == (bool(i % 2), float(i), i, i) + ok = ok and s.sampleno == i + 1 +check(ok, "9 samples read back in order") +check(s.num_underruns == 0, "no underruns while data remains") +check(s.read() is None, "read of an empty stream returns None") +check(s.num_underruns == 1, "underrun counted") + +s.close() +check(not s.is_open, "stream closed") +try: + s.readable + check(False, "access after close raises") +except RuntimeError: + check(True, "access after close raises") + h.exit() if HAVE_QUERY: check(not halpp.component_exists("halpp-test"), "exit removes component") diff --git a/tests/halpp/stream_reader.py b/tests/halpp/stream_reader.py new file mode 100755 index 00000000000..bac02c034aa --- /dev/null +++ b/tests/halpp/stream_reader.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# Attaches to the stream created by stream_writer.py and reads back the +# samples it wrote. +import time + +import halpp + +c = halpp.component("halpp_stream_reader") +reader = halpp.stream(c, halpp.streamer_base, "bfsu") +assert not reader.is_creator +assert reader.element_types == b"bfsu" +assert reader.maxdepth == 10 +for i in range(9): + assert reader.readable + assert reader.read() == (bool(i % 2), float(i), i, i) + assert reader.num_underruns == 0 + assert reader.sampleno == i + 1 +assert reader.read() is None +assert reader.num_underruns == 1 + +# An attach with a typestring the stream was not created with is refused. +try: + halpp.stream(c, halpp.streamer_base, "bfsf") +except OSError: + pass +else: + assert False, "attach with a mismatched typestring should fail" + +c.ready() +print("stream pass") + +try: + while 1: + time.sleep(1) +except KeyboardInterrupt: + pass diff --git a/tests/halpp/stream_writer.py b/tests/halpp/stream_writer.py new file mode 100755 index 00000000000..fd659c42a1d --- /dev/null +++ b/tests/halpp/stream_writer.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# Creates the stream that stream_reader.py attaches to, fills it, and +# stays loaded so the reader can map the same shared memory. +import time + +import halpp + +c = halpp.component("halpp_stream_writer") +writer = halpp.stream(c, halpp.streamer_base, 10, "bfsu") + +for i in range(9): + assert writer.writable + writer.write((i % 2, i, i, i)) +assert not writer.writable +assert writer.num_overruns == 0 +try: + writer.write((1, 1, 1, 1)) +except OSError: + pass +else: + assert False, "failed to get exception on full stream" +assert writer.num_overruns == 1 +c.ready() + +try: + while 1: + time.sleep(1) +except KeyboardInterrupt: + pass diff --git a/tests/halpp/test.sh b/tests/halpp/test.sh new file mode 100755 index 00000000000..81908f644b4 --- /dev/null +++ b/tests/halpp/test.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Python suite (component, pins/params, signals, by-name queries and +# streams) followed by the native C++ suite for hal.hh, compiled against +# the tree headers and run in a live HAL session. +halrun -f smoke.hal || exit 1 + +bindir=$(mktemp -d) +trap 'rm -rf "$bindir"' EXIT +g++ -std=gnu++20 -DULAPI -I"$EMC2_HOME/src/hal" -I"$HEADERS" \ + cpp_test.cc -o "$bindir/cpp_test" \ + -L"$LIBDIR" -Wl,-rpath,"$LIBDIR" -llinuxcnchal || exit 1 +halrun -I < Date: Sun, 9 Aug 2026 11:39:52 +1000 Subject: [PATCH 3/4] hal: register the _hal query submodule as hal.query Prototype of the registration mechanics only. _hal gains an empty query submodule and hal.py publishes it in sys.modules under its dotted name, so 'import hal.query', 'from hal import query', hal.query and _hal.query all resolve to the same object even though hal is a plain module rather than a package. The submodule carries no bindings yet; tests/halquery.0 checks the four access paths, which is what has to keep working once it does. --- lib/python/hal.py | 7 +++++++ src/hal/halmodule.cc | 12 ++++++++++++ tests/halquery.0/README | 3 +++ tests/halquery.0/expected | 6 ++++++ tests/halquery.0/test.py | 33 +++++++++++++++++++++++++++++++++ tests/halquery.0/test.sh | 3 +++ 6 files changed, 64 insertions(+) create mode 100644 tests/halquery.0/README create mode 100644 tests/halquery.0/expected create mode 100755 tests/halquery.0/test.py create mode 100755 tests/halquery.0/test.sh diff --git a/lib/python/hal.py b/lib/python/hal.py index e800dd3dcdf..630d129f74f 100644 --- a/lib/python/hal.py +++ b/lib/python/hal.py @@ -29,9 +29,16 @@ import _hal from _hal import * +from _hal import query +import sys import warnings import lcnc_realtime +# _hal.query is a submodule, not a plain attribute. Registering it in +# sys.modules under its dotted name makes 'import hal.query' and +# 'from hal import query' work as they would for a real package. +sys.modules['hal.query'] = query + def __getattr__(name): if name == 'is_rt': warnings.warn(f"{name} is deprecated, use lcnc_realtime.verify() instead", FutureWarning, stacklevel=2) diff --git a/src/hal/halmodule.cc b/src/hal/halmodule.cc index 4fe9f7165e0..13d03947115 100644 --- a/src/hal/halmodule.cc +++ b/src/hal/halmodule.cc @@ -2212,6 +2212,18 @@ PyMODINIT_FUNC PyInit__hal(void) PyModule_AddIntConstant(m, "streamer_base", 0x48535430); PyModule_AddIntConstant(m, "sampler_base", 0x48534130); + // The HAL query API is exposed as a submodule of _hal, which hal.py + // registers under the name hal.query. This is a placeholder that + // carries no bindings yet; it establishes where they will live. + PyObject *query = PyModule_New("_hal.query"); + if(!query) { + Py_DECREF(m); + return NULL; + } + PyModule_AddStringConstant(query, "__doc__", + "Interface to the HAL query API. No bindings yet."); + PyModule_AddObject(m, "query", query); + #ifdef RTAPI_KERNEL_VERSION PyModule_AddStringConstant(m, "kernel_version", RTAPI_KERNEL_VERSION); #else diff --git a/tests/halquery.0/README b/tests/halquery.0/README new file mode 100644 index 00000000000..b849ae81d13 --- /dev/null +++ b/tests/halquery.0/README @@ -0,0 +1,3 @@ +check that the query submodule of the _hal extension module is reachable as +hal.query: by attribute, by 'from hal import query', by 'import hal.query', +and directly as _hal.query. All four must yield the same module object. diff --git a/tests/halquery.0/expected b/tests/halquery.0/expected new file mode 100644 index 00000000000..1cde7f3331a --- /dev/null +++ b/tests/halquery.0/expected @@ -0,0 +1,6 @@ +ok - import _hal; _hal.query +ok - hal.query attribute access +ok - from hal import query +ok - import hal.query +ok - submodule keeps its own name +ALL TESTS PASSED diff --git a/tests/halquery.0/test.py b/tests/halquery.0/test.py new file mode 100755 index 00000000000..858ec672f3b --- /dev/null +++ b/tests/halquery.0/test.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# The HAL query API lives in the _hal extension module, but has to be +# reachable as hal.query. Check that all four ways of getting at it +# resolve to the same module object. +import sys + +failures = 0 + +def check(cond, msg): + global failures + if cond: + print("ok -", msg) + else: + print("FAIL -", msg) + failures += 1 + +import _hal +check(hasattr(_hal, "query"), "import _hal; _hal.query") + +import hal +check(hal.query is _hal.query, "hal.query attribute access") + +from hal import query +check(query is _hal.query, "from hal import query") + +import hal.query +check(sys.modules["hal.query"] is _hal.query, "import hal.query") +check(hal.query.__name__ == "_hal.query", "submodule keeps its own name") + +if failures: + print("%d FAILURES" % failures) + sys.exit(1) +print("ALL TESTS PASSED") diff --git a/tests/halquery.0/test.sh b/tests/halquery.0/test.sh new file mode 100755 index 00000000000..8d54e8f2f64 --- /dev/null +++ b/tests/halquery.0/test.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# No HAL instance needed: only module registration is inspected. +./test.py From d24bbd7a3e27e2331afef4dcade4eb2df15323bd Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:39:20 +0800 Subject: [PATCH 4/4] hal.type/hal.dir: native IntEnums for type and direction tagging Per Bertho's review, the tagging enums have a single source of truth in native code instead of a Python module that can drift from hal.h: - src/hal/halenum.hh builds enum.IntEnum subclasses through the plain Python C API (no pybind11 needed), with member values taken from the hal.h constants, so the classes cannot drift from the headers. Canonical members are the platform-stable spellings (BOOL, REAL, S32, U32, PORT, S64, U64 and IN..RW); SINT/UINT follow the platform width and all HAL_* spellings are aliases. - _hal registers the shared classes as _hal.type/_hal.dir (plus the HAL_WO constant, completing the direction set). hal.py serves hal.type/hal.dir and the HALType/HALDir class names lazily so the builtins stay usable inside the module; pickle round-trips through hal.HALType. - halpp drops its private py::enum_ copies, re-exports the shared classes and casts hal_type_t/hal::dir arguments and results through them: pin tags and stream element types print with their names and compare equal to the plain integer constants, which stay exported for compatibility. - tests/haltype.0 becomes a sanity suite for the classes; tests/halpp checks that halpp shares the hal.type/hal.dir objects and accepts the enums directly. TODO: when the oldest supported pybind11 is 3.0 or newer, construction can move to py::native_enum; the Python-visible classes stay enum.IntEnum either way, so user code will not notice. This replaces the haltype.py module, which is removed. --- lib/python/hal.py | 11 +++ src/hal/halenum.hh | 153 +++++++++++++++++++++++++++++++++++++++ src/hal/halmodule.cc | 16 ++++ src/hal/halpybind.cc | 112 +++++++++++++++++++++++----- tests/halpp/expected | 4 + tests/halpp/smoke.py | 8 ++ tests/haltype.0/README | 4 + tests/haltype.0/expected | 38 ++++++++++ tests/haltype.0/test.py | 87 ++++++++++++++++++++++ tests/haltype.0/test.sh | 3 + 10 files changed, 417 insertions(+), 19 deletions(-) create mode 100644 src/hal/halenum.hh create mode 100644 tests/haltype.0/README create mode 100644 tests/haltype.0/expected create mode 100755 tests/haltype.0/test.py create mode 100755 tests/haltype.0/test.sh diff --git a/lib/python/hal.py b/lib/python/hal.py index 630d129f74f..d0b18b74964 100644 --- a/lib/python/hal.py +++ b/lib/python/hal.py @@ -34,12 +34,23 @@ import warnings import lcnc_realtime +# _hal exports the IntEnum tagging classes as 'type' and 'dir'. The +# star import binds them here, but those names shadow the builtins used +# inside this module, so unbind them and serve them lazily through +# __getattr__ instead. hal.type.REAL, hal.dir.IN and the HALType/HALDir +# class names (also consulted by pickle) all resolve to the shared +# classes in _hal. +globals().pop('type', None) +globals().pop('dir', None) + # _hal.query is a submodule, not a plain attribute. Registering it in # sys.modules under its dotted name makes 'import hal.query' and # 'from hal import query' work as they would for a real package. sys.modules['hal.query'] = query def __getattr__(name): + if name in ('type', 'dir', 'HALType', 'HALDir'): + return getattr(_hal, {'HALType': 'type', 'HALDir': 'dir'}.get(name, name)) if name == 'is_rt': warnings.warn(f"{name} is deprecated, use lcnc_realtime.verify() instead", FutureWarning, stacklevel=2) return lcnc_realtime.verify() diff --git a/src/hal/halenum.hh b/src/hal/halenum.hh new file mode 100644 index 00000000000..67e3a8185a3 --- /dev/null +++ b/src/hal/halenum.hh @@ -0,0 +1,153 @@ +/* + halenum.hh - native IntEnum classes for HAL type/direction tagging + + Single source of truth for the Python-visible enums: the member + values are the hal.h constants themselves, so the classes cannot + drift from the C headers. Built through the plain Python C API (the + enum module's functional interface) so that any extension module can + create or instantiate them, with or without pybind11. + + _hal builds the two classes at module init and registers them as + _hal.type and _hal.dir; every other consumer fetches those shared + classes with halenum_shared_class() instead of building its own. + + TODO: when the oldest supported pybind11 is 3.0 or newer, class + construction can move to py::native_enum and instantiation to its + casters. The Python-visible classes stay enum.IntEnum either way, + so user code will not notice the switch. + + Note: this header is deliberately not exported to include/. The + header-sanity test preprocesses every exported header without + Python include paths, which does not survive. +*/ +#ifndef HALENUM_HH +#define HALENUM_HH + +#include + +#include + +struct halenum_member { + const char *name; + long value; +}; + +// Canonical members use the macro spellings where they are +// platform-stable (hal.h defines HAL_BOOL and HAL_REAL as macros over +// the enumerators). HAL_SINT and HAL_UINT are macros too, but they map +// to HAL_S32/HAL_U32 or HAL_S64/HAL_U64 depending on the platform, so +// the fixed-width spellings stay canonical and SINT/UINT follow as +// aliases. The HAL_* spellings are aliases as well. The enum module +// preserves dict order, so the first occurrence of each value is the +// canonical member. +static const halenum_member halenum_type_members[] = { + {"BOOL", HAL_BOOL}, + {"REAL", HAL_REAL}, + {"S32", HAL_S32}, + {"U32", HAL_U32}, + {"PORT", HAL_PORT}, + {"S64", HAL_S64}, + {"U64", HAL_U64}, + {"SINT", HAL_SINT}, + {"UINT", HAL_UINT}, + {"HAL_BOOL", HAL_BOOL}, + {"HAL_BIT", HAL_BIT}, + {"HAL_REAL", HAL_REAL}, + {"HAL_FLOAT", HAL_FLOAT}, + {"HAL_SINT", HAL_SINT}, + {"HAL_S32", HAL_S32}, + {"HAL_UINT", HAL_UINT}, + {"HAL_U32", HAL_U32}, + {"HAL_PORT", HAL_PORT}, + {"HAL_S64", HAL_S64}, + {"HAL_U64", HAL_U64}, +}; + +static const halenum_member halenum_dir_members[] = { + {"IN", HAL_IN}, + {"OUT", HAL_OUT}, + {"IO", HAL_IO}, + {"RO", HAL_RO}, + {"WO", HAL_WO}, + {"RW", HAL_RW}, + {"HAL_IN", HAL_IN}, + {"HAL_OUT", HAL_OUT}, + {"HAL_IO", HAL_IO}, + {"HAL_RO", HAL_RO}, + {"HAL_WO", HAL_WO}, + {"HAL_RW", HAL_RW}, +}; + +// Build an enum.IntEnum subclass from a member table. The class claims +// __module__ "hal", its public home, so repr() and pickle look right. +// Returns a new reference, or NULL with an exception set. +static inline PyObject *halenum_build(const char *clsname, + const halenum_member *members, size_t n) +{ + PyObject *enummod = PyImport_ImportModule("enum"); + if(!enummod) + return NULL; + PyObject *intenum = PyObject_GetAttrString(enummod, "IntEnum"); + Py_DECREF(enummod); + if(!intenum) + return NULL; + + PyObject *names = PyDict_New(); + if(!names) { + Py_DECREF(intenum); + return NULL; + } + for(size_t i = 0; i < n; i++) { + PyObject *v = PyLong_FromLong(members[i].value); + if(!v || PyDict_SetItemString(names, members[i].name, v)) { + Py_XDECREF(v); + Py_DECREF(names); + Py_DECREF(intenum); + return NULL; + } + Py_DECREF(v); + } + + PyObject *args = Py_BuildValue("(sO)", clsname, names); + PyObject *kwargs = Py_BuildValue("{ss}", "module", "hal"); + PyObject *cls = (args && kwargs) + ? PyObject_Call(intenum, args, kwargs) : NULL; + Py_XDECREF(args); + Py_XDECREF(kwargs); + Py_DECREF(names); + Py_DECREF(intenum); + return cls; +} + +static inline PyObject *halenum_make_type(void) +{ + return halenum_build("HALType", halenum_type_members, + sizeof(halenum_type_members)/sizeof(halenum_type_members[0])); +} + +static inline PyObject *halenum_make_dir(void) +{ + return halenum_build("HALDir", halenum_dir_members, + sizeof(halenum_dir_members)/sizeof(halenum_dir_members[0])); +} + +// Fetch one of the shared classes registered by _hal ("type" or "dir"), +// importing _hal if necessary. New reference. +static inline PyObject *halenum_shared_class(const char *attr) +{ + PyObject *m = PyImport_ImportModule("_hal"); + if(!m) + return NULL; + PyObject *cls = PyObject_GetAttrString(m, attr); + Py_DECREF(m); + return cls; +} + +// Instantiate a member: halenum_instance(cls, HAL_FLOAT) is HALType.REAL. +// New reference, or NULL with ValueError set for an unknown value. +static inline PyObject *halenum_instance(PyObject *cls, long value) +{ + return PyObject_CallFunction(cls, "l", value); +} + +#endif diff --git a/src/hal/halmodule.cc b/src/hal/halmodule.cc index 13d03947115..24eeac64bfc 100644 --- a/src/hal/halmodule.cc +++ b/src/hal/halmodule.cc @@ -27,6 +27,7 @@ #include #include #include "utils/setps_util.h" +#include "halenum.hh" #define EXCEPTION_IF_NOT_LIVE(retval) do { \ if(self->hal_id <= 0) { \ @@ -2191,11 +2192,26 @@ PyMODINIT_FUNC PyInit__hal(void) PyModule_AddIntConstant(m, "HAL_PORT", HAL_PORT); PyModule_AddIntConstant(m, "HAL_RO", HAL_RO); + PyModule_AddIntConstant(m, "HAL_WO", HAL_WO); PyModule_AddIntConstant(m, "HAL_RW", HAL_RW); PyModule_AddIntConstant(m, "HAL_IN", HAL_IN); PyModule_AddIntConstant(m, "HAL_OUT", HAL_OUT); PyModule_AddIntConstant(m, "HAL_IO", HAL_IO); + // IntEnum tagging classes for type and direction, built from the + // hal.h constants (see halenum.hh). Registered here so that every + // consumer, Python or C++, shares the same two classes. + PyObject *haltype = halenum_make_type(); + PyObject *haldir = halenum_make_dir(); + if(!haltype || !haldir + || PyModule_AddObject(m, "type", haltype) + || PyModule_AddObject(m, "dir", haldir)) { + Py_XDECREF(haltype); + Py_XDECREF(haldir); + Py_DECREF(m); + return NULL; + } + PyModule_AddIntConstant(m, "REALTIME_TYPE_UNINITIALIZED", REALTIME_TYPE_UNINITIALIZED); PyModule_AddIntConstant(m, "REALTIME_TYPE_NONE", REALTIME_TYPE_NONE); PyModule_AddIntConstant(m, "REALTIME_TYPE_UNKNOWN", REALTIME_TYPE_UNKNOWN); diff --git a/src/hal/halpybind.cc b/src/hal/halpybind.cc index 7b909b84dc1..dc870d80048 100644 --- a/src/hal/halpybind.cc +++ b/src/hal/halpybind.cc @@ -19,6 +19,7 @@ #include #include "hal.hh" +#include "halenum.hh" #ifdef HALXX_WITH_QUERY_API #include "utils/setps_util.h" @@ -27,6 +28,76 @@ namespace py = pybind11; namespace halxx = linuxcnc::hal; +namespace pybind11 { namespace detail { + +// Casts between the native enum values and the shared IntEnum classes +// registered by _hal (single source of truth: halenum.hh). Arguments +// accept the enums and plain ints alike; results come back as enum +// members, so tags print with their names. +template <> struct type_caster { + PYBIND11_TYPE_CASTER(hal_type_t, const_name("hal.type")); + + bool load(handle src, bool) { + PyObject *idx = PyNumber_Index(src.ptr()); + if(!idx) { + PyErr_Clear(); + return false; + } + long v = PyLong_AsLong(idx); + Py_DECREF(idx); + if(v == -1 && PyErr_Occurred()) { + PyErr_Clear(); + return false; + } + value = static_cast(v); + return true; + } + + static handle cast(hal_type_t v, return_value_policy, handle) { + PyObject *cls = halenum_shared_class("type"); + if(!cls) + throw pybind11::error_already_set(); + PyObject *obj = halenum_instance(cls, static_cast(v)); + Py_DECREF(cls); + if(!obj) + throw pybind11::error_already_set(); + return pybind11::reinterpret_steal(obj).release(); + } +}; + +template <> struct type_caster { + PYBIND11_TYPE_CASTER(linuxcnc::hal::dir, const_name("hal.dir")); + + bool load(handle src, bool) { + PyObject *idx = PyNumber_Index(src.ptr()); + if(!idx) { + PyErr_Clear(); + return false; + } + long v = PyLong_AsLong(idx); + Py_DECREF(idx); + if(v == -1 && PyErr_Occurred()) { + PyErr_Clear(); + return false; + } + value = static_cast(v); + return true; + } + + static handle cast(linuxcnc::hal::dir v, return_value_policy, handle) { + PyObject *cls = halenum_shared_class("dir"); + if(!cls) + throw pybind11::error_already_set(); + PyObject *obj = halenum_instance(cls, static_cast(v)); + Py_DECREF(cls); + if(!obj) + throw pybind11::error_already_set(); + return pybind11::reinterpret_steal(obj).release(); + } +}; + +}} // namespace pybind11::detail + #ifdef HALXX_WITH_QUERY_API // Text-to-value conversion is delegated to setps_common_cb so that // string parsing is consistent with halcmd setp/sets for all types. @@ -181,17 +252,28 @@ PYBIND11_MODULE(halpp, m) { return std::string(buf); }); - py::enum_(m, "hal_type_t") - .value("HAL_BIT", HAL_BIT) - .value("HAL_BOOL", HAL_BOOL) - .value("HAL_FLOAT", HAL_FLOAT) - .value("HAL_REAL", HAL_REAL) - .value("HAL_S32", HAL_S32) - .value("HAL_U32", HAL_U32) - .value("HAL_S64", HAL_S64) - .value("HAL_U64", HAL_U64) - .value("HAL_PORT", HAL_PORT) - .export_values(); + // Type and direction tags: the shared IntEnum classes from _hal, + // the same objects the casters above instantiate. The plain integer + // constants stay for compatibility; they compare equal to the enum + // members. + py::module_ halmod = py::module_::import("_hal"); + m.attr("type") = halmod.attr("type"); + m.attr("dir") = halmod.attr("dir"); + m.attr("HAL_BIT") = py::int_(static_cast(HAL_BIT)); + m.attr("HAL_BOOL") = py::int_(static_cast(HAL_BOOL)); + m.attr("HAL_FLOAT") = py::int_(static_cast(HAL_FLOAT)); + m.attr("HAL_REAL") = py::int_(static_cast(HAL_REAL)); + m.attr("HAL_S32") = py::int_(static_cast(HAL_S32)); + m.attr("HAL_U32") = py::int_(static_cast(HAL_U32)); + m.attr("HAL_S64") = py::int_(static_cast(HAL_S64)); + m.attr("HAL_U64") = py::int_(static_cast(HAL_U64)); + m.attr("HAL_PORT") = py::int_(static_cast(HAL_PORT)); + m.attr("HAL_IN") = py::int_(static_cast(HAL_IN)); + m.attr("HAL_OUT") = py::int_(static_cast(HAL_OUT)); + m.attr("HAL_IO") = py::int_(static_cast(HAL_IO)); + m.attr("HAL_RO") = py::int_(static_cast(HAL_RO)); + m.attr("HAL_WO") = py::int_(static_cast(HAL_WO)); + m.attr("HAL_RW") = py::int_(static_cast(HAL_RW)); // 'halcmd unload' terminates a userspace component with SIGTERM. // Raise KeyboardInterrupt for it, as the _hal module does, so the @@ -199,12 +281,4 @@ PYBIND11_MODULE(halpp, m) { // dying where it stands. py::module_ signal = py::module_::import("signal"); signal.attr("signal")(signal.attr("SIGTERM"), signal.attr("default_int_handler")); - - py::enum_(m, "hal_dir") - .value("HAL_IN", halxx::dir::IN) - .value("HAL_OUT", halxx::dir::OUT) - .value("HAL_IO", halxx::dir::IO) - .value("HAL_RO", halxx::dir::RO) - .value("HAL_RW", halxx::dir::RW) - .export_values(); } diff --git a/tests/halpp/expected b/tests/halpp/expected index 2b63d4e0336..a9abcf4f8c3 100644 --- a/tests/halpp/expected +++ b/tests/halpp/expected @@ -1,4 +1,7 @@ ok - component created +ok - halpp.type is the shared hal.type class +ok - halpp.dir is the shared hal.dir class +ok - newpin accepts the enums directly ok - port pin creation raises until API break ok - param set/get roundtrip ok - bit pin set/get @@ -22,6 +25,7 @@ ok - stream with an invalid typestring raises ok - element_types: b'bfsu' ok - element_count ok - element_type +ok - element_type returns the enum member ok - maxdepth is the depth the stream was created with ok - creator flag ok - key diff --git a/tests/halpp/smoke.py b/tests/halpp/smoke.py index ff539a7f849..ef7ffe71df1 100755 --- a/tests/halpp/smoke.py +++ b/tests/halpp/smoke.py @@ -21,6 +21,13 @@ def check(cond, msg): h = halpp.component("halpp-test") check(isinstance(h.id, int) or True, "component created") +# --- shared type/dir tagging enums (halenum.hh) --------------------------- +import hal +check(halpp.type is hal.type, "halpp.type is the shared hal.type class") +check(halpp.dir is hal.dir, "halpp.dir is the shared hal.dir class") +p_tag = h.newpin("tag-in", halpp.type.REAL, halpp.dir.IN) +check(p_tag.name == "halpp-test.tag-in", "newpin accepts the enums directly") + # --- typed pins via runtime type dispatch -------------------------------- p_bit = h.newpin("bit-out", halpp.HAL_BIT, halpp.HAL_OUT) p_f = h.newpin("float-in", halpp.HAL_FLOAT, halpp.HAL_IN) @@ -103,6 +110,7 @@ def check(cond, msg): check(s.element_types == b"bfsu", "element_types: " + repr(s.element_types)) check(s.element_count == 4, "element_count") check(s.element_type(1) == halpp.HAL_FLOAT, "element_type") +check(s.element_type(1) is halpp.type.REAL, "element_type returns the enum member") check(s.maxdepth == 10, "maxdepth is the depth the stream was created with") check(s.is_creator, "creator flag") check(s.key == halpp.streamer_base, "key") diff --git a/tests/haltype.0/README b/tests/haltype.0/README new file mode 100644 index 00000000000..6ce20a3ca18 --- /dev/null +++ b/tests/haltype.0/README @@ -0,0 +1,4 @@ +sanity checks for the native IntEnum tagging classes hal.type and hal.dir, +built from the hal.h constants by halenum.hh and shared by all consumers +(_hal, hal.py, the halpp bindings). Covers canonical members, aliases, +integer interoperability, construction from values, and pickling. diff --git a/tests/haltype.0/expected b/tests/haltype.0/expected new file mode 100644 index 00000000000..a372412fb3c --- /dev/null +++ b/tests/haltype.0/expected @@ -0,0 +1,38 @@ +ok - hal.type canonical members +ok - hal.dir canonical members +ok - hal.type.BOOL == _hal.HAL_BOOL (1) +ok - hal.type.REAL == _hal.HAL_REAL (2) +ok - hal.type.S32 == _hal.HAL_S32 (3) +ok - hal.type.U32 == _hal.HAL_U32 (4) +ok - hal.type.PORT == _hal.HAL_PORT (5) +ok - hal.type.S64 == _hal.HAL_S64 (6) +ok - hal.type.U64 == _hal.HAL_U64 (7) +ok - hal.type.SINT == _hal.HAL_SINT (6) +ok - hal.type.UINT == _hal.HAL_UINT (7) +ok - hal.dir.IN == _hal.HAL_IN (16) +ok - hal.dir.OUT == _hal.HAL_OUT (32) +ok - hal.dir.IO == _hal.HAL_IO (48) +ok - hal.dir.RO == _hal.HAL_RO (64) +ok - hal.dir.WO == _hal.HAL_WO (128) +ok - hal.dir.RW == _hal.HAL_RW (192) +ok - HAL_REAL aliases REAL +ok - HAL_FLOAT aliases REAL +ok - HAL_BIT aliases BOOL +ok - HAL_S32 aliases S32 +ok - HAL_SINT aliases SINT +ok - HAL_IN aliases IN +ok - HAL_RW aliases RW +ok - hal.type.REAL is an int +ok - hal.type.REAL == hal.HAL_FLOAT +ok - hal.dir.IO is IN|OUT +ok - hal.dir.RW is RO|WO +ok - hal.type is _hal.type +ok - hal.dir is _hal.dir +ok - hal.HALType is hal.type +ok - hal.HALDir is hal.dir +ok - hal.type(2) is REAL +ok - hal.dir(16) is IN +ok - hal.type(99) raises ValueError +ok - repr shows the member name +ok - hal.type.REAL survives pickle +ALL TESTS PASSED diff --git a/tests/haltype.0/test.py b/tests/haltype.0/test.py new file mode 100755 index 00000000000..06a256da0a6 --- /dev/null +++ b/tests/haltype.0/test.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# Sanity checks for the native IntEnum tagging classes (halenum.hh). +# +# The classes are built from the hal.h constants in _hal and shared by +# every consumer: hal.type/hal.dir, the HALType/HALDir class names, the +# _hal module itself, and the halpp bindings (covered in tests/halpp). +import pickle +import sys + +import _hal +import hal + +failures = 0 + +def check(cond, msg): + global failures + if cond: + print("ok -", msg) + else: + print("FAIL -", msg) + failures += 1 + +# Canonical members: the fixed-width spellings, one per value, in hal.h +# order. SINT/UINT map to 32 or 64 bits depending on the platform, so +# they are aliases, not canonical members. +check([m.name for m in hal.type] == + ["BOOL", "REAL", "S32", "U32", "PORT", "S64", "U64"], + "hal.type canonical members") +check([m.name for m in hal.dir] == + ["IN", "OUT", "IO", "RO", "WO", "RW"], + "hal.dir canonical members") + +# Values come straight from the hal.h constants exported by _hal. +for name in ("BOOL", "REAL", "S32", "U32", "PORT", "S64", "U64", + "SINT", "UINT"): + member = hal.type[name] + const = getattr(_hal, "HAL_" + name) + check(int(member) == const, + "hal.type.%s == _hal.HAL_%s (%d)" % (name, name, const)) +for name in ("IN", "OUT", "IO", "RO", "WO", "RW"): + member = hal.dir[name] + const = getattr(_hal, "HAL_" + name) + check(int(member) == const, + "hal.dir.%s == _hal.HAL_%s (%d)" % (name, name, const)) + +# The HAL_* spellings are aliases of the canonical members, both the +# enumerator and the macro spelling. +check(hal.type.HAL_REAL is hal.type.REAL, "HAL_REAL aliases REAL") +check(hal.type.HAL_FLOAT is hal.type.REAL, "HAL_FLOAT aliases REAL") +check(hal.type.HAL_BIT is hal.type.BOOL, "HAL_BIT aliases BOOL") +check(hal.type.HAL_S32 is hal.type.S32, "HAL_S32 aliases S32") +check(hal.type.HAL_SINT is hal.type.SINT, "HAL_SINT aliases SINT") +check(hal.dir.HAL_IN is hal.dir.IN, "HAL_IN aliases IN") +check(hal.dir.HAL_RW is hal.dir.RW, "HAL_RW aliases RW") + +# IntEnum members are ints and interoperate with the plain constants. +check(isinstance(hal.type.REAL, int), "hal.type.REAL is an int") +check(hal.type.REAL == hal.HAL_FLOAT, "hal.type.REAL == hal.HAL_FLOAT") +check(hal.dir.IO == hal.dir.IN | hal.dir.OUT, "hal.dir.IO is IN|OUT") +check(hal.dir.RW == hal.dir.RO | hal.dir.WO, "hal.dir.RW is RO|WO") + +# One shared pair of classes, reachable from every spelling. +check(hal.type is _hal.type, "hal.type is _hal.type") +check(hal.dir is _hal.dir, "hal.dir is _hal.dir") +check(hal.HALType is hal.type, "hal.HALType is hal.type") +check(hal.HALDir is hal.dir, "hal.HALDir is hal.dir") + +# Construction from a value, the way query results are tagged. +check(hal.type(2) is hal.type.REAL, "hal.type(2) is REAL") +check(hal.dir(16) is hal.dir.IN, "hal.dir(16) is IN") +try: + hal.type(99) + check(False, "hal.type(99) raises ValueError") +except ValueError: + check(True, "hal.type(99) raises ValueError") + +# Tags print with their names, not bare numbers. +check(repr(hal.type.REAL) == "", "repr shows the member name") + +# Pickle round-trips through hal.HALType (the class __module__ is "hal"). +check(pickle.loads(pickle.dumps(hal.type.REAL)) is hal.type.REAL, + "hal.type.REAL survives pickle") + +if failures: + print("%d FAILURES" % failures) + sys.exit(1) +print("ALL TESTS PASSED") diff --git a/tests/haltype.0/test.sh b/tests/haltype.0/test.sh new file mode 100755 index 00000000000..cc490858274 --- /dev/null +++ b/tests/haltype.0/test.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# No HAL instance needed: only module-level constants are inspected. +./test.py