From 87ff92ba6c145d38959def706f823309b38c5a85 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:51:10 +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. Typed pin/param handles via traits (compile-time accessor selection for the rtapi_ types), runtime-typed pin_t variant and anypin for name-based access, port wrapper, component class, 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). 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. - taskclass converted to the new API (it was the only hal.hh user). - 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/emc/task/taskclass.cc | 35 +- src/hal/Submakefile | 13 + src/hal/hal.hh | 770 +++++++++++++++++++++++++++++++------- src/hal/halpybind.cc | 107 ++++++ tests/halpp/cpp_test.cc | 90 +++++ tests/halpp/smoke.py | 105 ++++++ 7 files changed, 980 insertions(+), 141 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/emc/task/taskclass.cc b/src/emc/task/taskclass.cc index 9e03dcd6940..a100058f4e0 100644 --- a/src/emc/task/taskclass.cc +++ b/src/emc/task/taskclass.cc @@ -40,23 +40,26 @@ using namespace linuxcnc; ********************************************************************/ int Task::iocontrol_hal_init(void) { - iocontrol.add_pin("user-enable-out", hal_dir::OUT, iocontrol_data.user_enable_out); - iocontrol.add_pin("emc-enable-in", hal_dir::IN, iocontrol_data.emc_enable_in); - iocontrol.add_pin("user-request-enable", hal_dir::OUT, iocontrol_data.user_request_enable); - iocontrol.add_pin("coolant-mist", hal_dir::OUT, iocontrol_data.coolant_mist); - iocontrol.add_pin("coolant-flood", hal_dir::OUT, iocontrol_data.coolant_flood); - iocontrol.add_pin("tool-prep-pocket", hal_dir::OUT, iocontrol_data.tool_prep_pocket); - iocontrol.add_pin("tool-from-pocket", hal_dir::OUT, iocontrol_data.tool_from_pocket); - iocontrol.add_pin("tool-prep-index", hal_dir::OUT, iocontrol_data.tool_prep_index); - iocontrol.add_pin("tool-prep-number", hal_dir::OUT, iocontrol_data.tool_prep_number); - iocontrol.add_pin("tool-number", hal_dir::OUT, iocontrol_data.tool_number); - iocontrol.add_pin("tool-prepare", hal_dir::OUT, iocontrol_data.tool_prepare); - iocontrol.add_pin("tool-prepared", hal_dir::IN, iocontrol_data.tool_prepared); - iocontrol.add_pin("tool-change", hal_dir::OUT, iocontrol_data.tool_change); - iocontrol.add_pin("tool-changed", hal_dir::IN, iocontrol_data.tool_changed); - iocontrol.ready(); - if (iocontrol.error < 0) + try { + iocontrol.add_pin("user-enable-out", hal_dir::OUT, iocontrol_data.user_enable_out); + iocontrol.add_pin("emc-enable-in", hal_dir::IN, iocontrol_data.emc_enable_in); + iocontrol.add_pin("user-request-enable", hal_dir::OUT, iocontrol_data.user_request_enable); + iocontrol.add_pin("coolant-mist", hal_dir::OUT, iocontrol_data.coolant_mist); + iocontrol.add_pin("coolant-flood", hal_dir::OUT, iocontrol_data.coolant_flood); + iocontrol.add_pin("tool-prep-pocket", hal_dir::OUT, iocontrol_data.tool_prep_pocket); + iocontrol.add_pin("tool-from-pocket", hal_dir::OUT, iocontrol_data.tool_from_pocket); + iocontrol.add_pin("tool-prep-index", hal_dir::OUT, iocontrol_data.tool_prep_index); + iocontrol.add_pin("tool-prep-number", hal_dir::OUT, iocontrol_data.tool_prep_number); + iocontrol.add_pin("tool-number", hal_dir::OUT, iocontrol_data.tool_number); + iocontrol.add_pin("tool-prepare", hal_dir::OUT, iocontrol_data.tool_prepare); + iocontrol.add_pin("tool-prepared", hal_dir::IN, iocontrol_data.tool_prepared); + iocontrol.add_pin("tool-change", hal_dir::OUT, iocontrol_data.tool_change); + iocontrol.add_pin("tool-changed", hal_dir::IN, iocontrol_data.tool_changed); + iocontrol.ready(); + } catch(const std::exception &e) { + fprintf(stderr, "iocontrol_hal_init: %s\n", e.what()); return -1; + } return 0; } diff --git a/src/hal/Submakefile b/src/hal/Submakefile index 85b4c56f121..0bed63c1288 100644 --- a/src/hal/Submakefile +++ b/src/hal/Submakefile @@ -28,5 +28,18 @@ $(HALMODULE): $(call TOOBJS, $(HALMODULESRCS)) $(HALLIB) $(ECHO) Linking python module $(notdir $@) $(Q)$(CXX) $(LDFLAGS) -shared -o $@ $^ +# pybind11 C++ bindings (hal.hh based) +HALPPSRCS := hal/halpybind.cc +PYSRCS += $(HALPPSRCS) + +# pybind11 headers: python3-pybind11 (system include) or pip user site +$(call TOOBJS, $(HALPPSRCS)): EXTRAFLAGS += $(shell python3 -m pybind11 --includes 2>/dev/null) + +HALPP := ../lib/python/halpp.so +$(HALPP): $(call TOOBJS, $(HALPPSRCS)) $(HALLIB) + $(ECHO) Linking python module $(notdir $@) + $(Q)$(CXX) $(LDFLAGS) -shared -o $@ $^ + TARGETS += $(HALLIB) ../lib/liblinuxcnchal.so.0 PYTARGETS += $(HALMODULE) +PYTARGETS += $(HALPP) diff --git a/src/hal/hal.hh b/src/hal/hal.hh index 3e4af13a4e6..1034197c73e 100644 --- a/src/hal/hal.hh +++ b/src/hal/hal.hh @@ -1,166 +1,686 @@ +/* + 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. +*/ #ifndef HALXX_HH #define HALXX_HH #include #include #include -#include "hal.h" +#include +#include +#include +#include + +#include + +// 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 -enum class hal_dir{ - IN = HAL_IN, +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_BIT; + 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()); + } +}; + +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()); + } + 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()); + } +}; + +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()); + } + 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()); + } +}; + +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()); } }; -#endif +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 hal_pin{ - public: - volatile T** ptr; - T operator=(const T& value){ - **ptr = value; - return **ptr; +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; +}; + +//---------------------------------------------------------------------- +// port - HAL_PORT pin handle. +// +// A port is not a normal pin: it is a FIFO buffer endpoint. There is +// one IN and one OUT port pin and they connect to a signal of type +// HAL_PORT. You cannot assign to a port pin or read a scalar value +// from it (a scalar read yields the queue reference, not data). +// +// Note: the opaque hal_port_t handle type cannot be used during the +// transition period because it conflicts with the legacy hal_port_t* +// form; the handle slot is therefore held as hal_sint_t until the API +// break, after which this class becomes a hal_port_t wrapper. +//---------------------------------------------------------------------- +class port { +public: + port() = default; + explicit port(hal_sint_t *slot) : slot_(slot) {} + port(const port &) = delete; + port &operator=(const port &) = delete; + port(port &&) = default; + port &operator=(port &&) = default; + + unsigned size() const { return hal_port_buffer_size(p()); } + unsigned readable() const { return hal_port_readable(p()); } + unsigned writable() const { return hal_port_writable(p()); } + + bool write(const std::string &data) { + return hal_port_write(p(), data.c_str(), (unsigned)data.length()); } - operator T(){ - return **ptr; + bool write(const char *data, unsigned count) { + return hal_port_write(p(), data, count); } + std::string read(unsigned count) { + std::string buf(count, '\0'); + if(count && hal_port_read(p(), buf.data(), count)) + return buf; + return {}; + } + std::string peek(unsigned count) { + std::string buf(count, '\0'); + if(count && hal_port_peek(p(), buf.data(), count)) + return buf; + return {}; + } + + hal_sint_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::port: access to uninitialized port handle"); + } + const hal_port_t *p() const { check(); return reinterpret_cast(*slot_); } + hal_sint_t *slot_ = nullptr; }; -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); - } +//---------------------------------------------------------------------- +// 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, port>; + +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 { + using P = std::decay_t; + if constexpr (std::is_same_v) + throw std::invalid_argument("hal: port pin has no scalar value"); + else + 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; + if constexpr (std::is_same_v) + throw std::invalid_argument("hal: port pin has no scalar value"); + else + 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 { + using P = std::decay_t; + if constexpr (std::is_same_v) + return HAL_PORT; + else + return traits::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_); } + bool is_port() const { return std::holds_alternative(p_); } + + port &as_port() { + if(auto *pp = std::get_if(&p_)) + return *pp; + throw std::invalid_argument("hal::anypin: not a port pin"); } - hal_comp() = delete; - 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; - } + 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_; } - 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; + 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)); + } + + // 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_BIT: 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)); +#ifdef HALXX_WITH_QUERY_API + case HAL_PORT: { + 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 = hal_pin_new_port(id_, (hal_pin_dir_t)d, &u->s, "%s", fullname(name).c_str()); + if(rv) + throw std::runtime_error("hal::component: newpin(" + name + ") failed: " + detail::errstr(rv)); + items_.emplace(name, port(&u->s)); + return anypin(port(&u->s), fullname(name)); + } +#endif + 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_BIT: 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); } - ~hal_comp(){ - hal_exit(comp_id); + 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). +// Only available in user-land when the query API is present. +//---------------------------------------------------------------------- +#if defined(ULAPI) && defined(HALXX_WITH_QUERY_API) + +namespace detail { + +// 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_BIT: + 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_BIT: 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_get_p(&q, nullptr, nullptr) || !q.pp.signal) + return false; + hal_query_t sq = {}; + sq.name = q.pp.signal; + if(0 != hal_get_s(&sq, nullptr, nullptr)) + return false; + return sq.sig.writers > 0; +} + +// Read the value of a pin, param or signal by name. Throws +// std::invalid_argument if the name does not exist. +inline value_t get_value(const std::string &name) +{ + hal_query_t q = {}; + q.name = name.c_str(); + if(0 == hal_get_p(&q, nullptr, nullptr)) + return detail::value_from_query(q.pp.type, q.pp.value); + if(0 == hal_get_s(&q, nullptr, nullptr)) + return detail::value_from_query(q.sig.type, q.sig.value); + throw std::invalid_argument("hal: pin, param or signal '" + name + "' not found"); +} + +// 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..c14c5c575ba --- /dev/null +++ b/src/hal/halpybind.cc @@ -0,0 +1,107 @@ +/* + 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" + +namespace py = pybind11; +namespace halxx = linuxcnc::hal; + +PYBIND11_MODULE(halpp, m) { + m.doc() = "Interface to linuxcnc hal"; + +#ifdef HALXX_WITH_QUERY_API + // 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_p", &halxx::set_value); // compatibility name + m.def("set_signal", &halxx::set_signal); +#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) + .def("is_port", &halxx::anypin::is_port) + // HAL_PORT access. These throw ValueError on non-port pins. + .def("write", [](halxx::anypin &p, const std::string &d) { return p.as_port().write(d); }) + .def("read", [](halxx::anypin &p, unsigned c) { return py::bytes(p.as_port().read(c)); }) + .def("peek", [](halxx::anypin &p, unsigned c) { return py::bytes(p.as_port().peek(c)); }) + .def("size", [](halxx::anypin &p) { return p.as_port().size(); }) + .def("writable", [](halxx::anypin &p) { return p.as_port().writable(); }) + .def("readable", [](halxx::anypin &p) { return p.as_port().readable(); }); + + 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_FLOAT", HAL_FLOAT) + .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..508cee5cc7d --- /dev/null +++ b/tests/halpp/smoke.py @@ -0,0 +1,105 @@ +#!/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) +try: + p_port = h.newpin("port-out", halpp.HAL_PORT, halpp.HAL_OUT) +except ValueError: + p_port = None # HAL_PORT creation requires the query API + +# --- 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") + +# --- port ------------------------------------------------------------------ +if p_port is not None: + check(p_port.is_port(), "port pin is_port") +try: + p_bit.as_port() + check(False, "non-port as_port raises") +except (ValueError, Exception): + check(True, "non-port as_port raises") + +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 17a648f8ab5436692e47932095fa410f11b88908 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:36:40 +0800 Subject: [PATCH 2/4] halpp: library init/exit and setps_common_cb string conversion Initialize the user-land HAL library at module import and register hal_lib_exit() with Py_AtExit so forgotten component exits are reported (query API tree only, where these exist). String overloads of set_value/set_p/set_signal delegate text-to-value conversion to setps_common_cb for consistency with halcmd setp/sets (boolean names, locale-safe floats, range checks). setps_util.c is linked via $(wildcard) so the build works with and without it. --- src/hal/Submakefile | 3 ++- src/hal/halpybind.cc | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/hal/Submakefile b/src/hal/Submakefile index 0bed63c1288..d672d7596bc 100644 --- a/src/hal/Submakefile +++ b/src/hal/Submakefile @@ -29,7 +29,8 @@ $(HALMODULE): $(call TOOBJS, $(HALMODULESRCS)) $(HALLIB) $(Q)$(CXX) $(LDFLAGS) -shared -o $@ $^ # pybind11 C++ bindings (hal.hh based) -HALPPSRCS := hal/halpybind.cc +# setps_util.c only exists in the tree with the HAL query API +HALPPSRCS := hal/halpybind.cc $(wildcard hal/utils/setps_util.c) PYSRCS += $(HALPPSRCS) # pybind11 headers: python3-pybind11 (system include) or pip user site diff --git a/src/hal/halpybind.cc b/src/hal/halpybind.cc index c14c5c575ba..4229985c038 100644 --- a/src/hal/halpybind.cc +++ b/src/hal/halpybind.cc @@ -16,21 +16,58 @@ #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 From e3472f05cbd3d48a90acd0c2dae11dac64aef95c Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:48:18 +0800 Subject: [PATCH 3/4] halxx: second review round - Ports transfer binary data: read/write/peek use std::vector, bindings expose bytes - Port pins have a scalar value: reads yield the queue reference (via hal_get_sint until hal_get_port() lands), writes are rejected - Port creation is core hal lib; comment corrected, still gated on availability of hal_pin_new_port() - By-name section documented user-space-only by definition - Type constants use the new names HAL_BOOL/HAL_REAL throughout - pin_has_writer uses hal_getref_p/hal_getref_s (no value read) - get_value propagates the library error instead of a generic not-found --- src/hal/hal.hh | 55 ++++++++++++++++++++++++++------------------ src/hal/halpybind.cc | 20 ++++++++++++---- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/hal/hal.hh b/src/hal/hal.hh index 1034197c73e..fe762dae97a 100644 --- a/src/hal/hal.hh +++ b/src/hal/hal.hh @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -82,7 +83,7 @@ template struct traits; 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_BIT; + 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) { @@ -236,20 +237,21 @@ public: unsigned readable() const { return hal_port_readable(p()); } unsigned writable() const { return hal_port_writable(p()); } - bool write(const std::string &data) { - return hal_port_write(p(), data.c_str(), (unsigned)data.length()); + // Ports transfer binary data, not strings. + bool write(const std::vector &data) { + return hal_port_write(p(), data.data(), (unsigned)data.size()); } bool write(const char *data, unsigned count) { return hal_port_write(p(), data, count); } - std::string read(unsigned count) { - std::string buf(count, '\0'); + std::vector read(unsigned count) { + std::vector buf(count); if(count && hal_port_read(p(), buf.data(), count)) return buf; return {}; } - std::string peek(unsigned count) { - std::string buf(count, '\0'); + std::vector peek(unsigned count) { + std::vector buf(count); if(count && hal_port_peek(p(), buf.data(), count)) return buf; return {}; @@ -283,10 +285,15 @@ inline value_t pin_get(const pin_t &p) { return std::visit([](auto &&pp) -> value_t { using P = std::decay_t; - if constexpr (std::is_same_v) - throw std::invalid_argument("hal: port pin has no scalar value"); - else + if constexpr (std::is_same_v) { + // A port pin does have a scalar value: reading yields the + // queue reference (the port's value storage), writing is + // not allowed. Read through the sint accessor until the + // port change makes hal_get_port() available. + return (rtapi_sint)hal_get_sint(pp.handle()); + } else { return pp.get(); + } }, p); } @@ -295,7 +302,7 @@ inline void pin_set(pin_t &p, const value_t &v) std::visit([&v](auto &&pp) { using P = std::decay_t; if constexpr (std::is_same_v) - throw std::invalid_argument("hal: port pin has no scalar value"); + throw std::invalid_argument("hal: cannot write to a port pin"); else pp.set(std::visit([](auto &&x) -> typename P::value_type { return static_cast(x); @@ -409,13 +416,15 @@ public: // 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_BIT: return wrap(name, newpin(name, d)); + 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)); #ifdef HALXX_WITH_QUERY_API + // Ports are created with hal_pin_new_port(), which is core HAL + // library but not present in the base getter/setter tree. case HAL_PORT: { hal_refs_u *u = (hal_refs_u *)hal_malloc(sizeof(*u)); if(!u) @@ -447,7 +456,7 @@ public: anypin newparam(const std::string &name, hal_type_t type, dir d) { switch(type) { - case HAL_BIT: return wrap(name, newparam(name, d)); + 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)); @@ -516,7 +525,8 @@ inline int signal_delete(const std::string &name) //---------------------------------------------------------------------- // 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). -// Only available in user-land when the query API is present. +// 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) @@ -533,7 +543,7 @@ inline bool convert_value(hal_type_t target, const value_t &v, hal_query_value_u std::visit([&ok, out, target](auto &&x) { long double xv = static_cast(x); switch(target) { - case HAL_BIT: + case HAL_BOOL: out->b = (0 != xv); break; case HAL_S32: @@ -560,7 +570,7 @@ inline bool convert_value(hal_type_t target, const value_t &v, hal_query_value_u inline value_t value_from_query(hal_type_t t, const hal_query_value_u &v) { switch(t) { - case HAL_BIT: return (rtapi_bool)v.b; + 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; @@ -620,26 +630,27 @@ 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_get_p(&q, nullptr, nullptr) || !q.pp.signal) + if(0 != hal_getref_p(&q) || !q.pp.signal) return false; hal_query_t sq = {}; sq.name = q.pp.signal; - if(0 != hal_get_s(&sq, nullptr, nullptr)) + 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 if the name does not exist. +// 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(); - if(0 == hal_get_p(&q, nullptr, nullptr)) + int rv = hal_get_p(&q, nullptr, nullptr); + if(0 == rv) return detail::value_from_query(q.pp.type, q.pp.value); - if(0 == hal_get_s(&q, nullptr, nullptr)) + 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: pin, param or signal '" + name + "' not found"); + 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 diff --git a/src/hal/halpybind.cc b/src/hal/halpybind.cc index 4229985c038..36cd050a1cc 100644 --- a/src/hal/halpybind.cc +++ b/src/hal/halpybind.cc @@ -91,10 +91,20 @@ PYBIND11_MODULE(halpp, m) { .def_property_readonly("name", &halxx::anypin::name) .def("get_name", &halxx::anypin::name) .def("is_port", &halxx::anypin::is_port) - // HAL_PORT access. These throw ValueError on non-port pins. - .def("write", [](halxx::anypin &p, const std::string &d) { return p.as_port().write(d); }) - .def("read", [](halxx::anypin &p, unsigned c) { return py::bytes(p.as_port().read(c)); }) - .def("peek", [](halxx::anypin &p, unsigned c) { return py::bytes(p.as_port().peek(c)); }) + // HAL_PORT access. Binary data, exposed as bytes. These throw + // ValueError on non-port pins. + .def("write", [](halxx::anypin &p, py::bytes d) { + std::string s = d; + return p.as_port().write(s.data(), (unsigned)s.size()); + }) + .def("read", [](halxx::anypin &p, unsigned c) { + auto v = p.as_port().read(c); + return py::bytes(std::string(v.begin(), v.end())); + }) + .def("peek", [](halxx::anypin &p, unsigned c) { + auto v = p.as_port().peek(c); + return py::bytes(std::string(v.begin(), v.end())); + }) .def("size", [](halxx::anypin &p) { return p.as_port().size(); }) .def("writable", [](halxx::anypin &p) { return p.as_port().writable(); }) .def("readable", [](halxx::anypin &p) { return p.as_port().readable(); }); @@ -126,7 +136,9 @@ PYBIND11_MODULE(halpp, m) { 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) From 202386a1f36c5eeaad69fad9a986e691670d5b0d Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:52:01 +0800 Subject: [PATCH 4/4] halxx: exempt HAL_PORT until the API break Port handle type and creation semantics are still in flux (opaque hal_port_t conflicts with the legacy form, hal_get_port is disabled, hal_pin_new_port does not exist in the base tree), so a port wrapper written now would be transitional glue that gets redone at the break. Remove the port class, the variant alternative and the bindings; document the exemption. Ports return post-break as a hal_port_t based wrapper. --- src/hal/hal.hh | 114 +++++-------------------------------------- src/hal/halpybind.cc | 20 +------- tests/halpp/smoke.py | 16 ++---- 3 files changed, 17 insertions(+), 133 deletions(-) diff --git a/src/hal/hal.hh b/src/hal/hal.hh index fe762dae97a..c748a39b4ca 100644 --- a/src/hal/hal.hh +++ b/src/hal/hal.hh @@ -13,6 +13,11 @@ 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 @@ -211,71 +216,13 @@ private: handle_t *slot_ = nullptr; }; -//---------------------------------------------------------------------- -// port - HAL_PORT pin handle. -// -// A port is not a normal pin: it is a FIFO buffer endpoint. There is -// one IN and one OUT port pin and they connect to a signal of type -// HAL_PORT. You cannot assign to a port pin or read a scalar value -// from it (a scalar read yields the queue reference, not data). -// -// Note: the opaque hal_port_t handle type cannot be used during the -// transition period because it conflicts with the legacy hal_port_t* -// form; the handle slot is therefore held as hal_sint_t until the API -// break, after which this class becomes a hal_port_t wrapper. -//---------------------------------------------------------------------- -class port { -public: - port() = default; - explicit port(hal_sint_t *slot) : slot_(slot) {} - port(const port &) = delete; - port &operator=(const port &) = delete; - port(port &&) = default; - port &operator=(port &&) = default; - - unsigned size() const { return hal_port_buffer_size(p()); } - unsigned readable() const { return hal_port_readable(p()); } - unsigned writable() const { return hal_port_writable(p()); } - - // Ports transfer binary data, not strings. - bool write(const std::vector &data) { - return hal_port_write(p(), data.data(), (unsigned)data.size()); - } - bool write(const char *data, unsigned count) { - return hal_port_write(p(), data, count); - } - std::vector read(unsigned count) { - std::vector buf(count); - if(count && hal_port_read(p(), buf.data(), count)) - return buf; - return {}; - } - std::vector peek(unsigned count) { - std::vector buf(count); - if(count && hal_port_peek(p(), buf.data(), count)) - return buf; - return {}; - } - - hal_sint_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::port: access to uninitialized port handle"); - } - const hal_port_t *p() const { check(); return reinterpret_cast(*slot_); } - hal_sint_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, port>; + pin, pin, pin>; namespace detail { @@ -283,41 +230,23 @@ namespace detail { // scalar value. inline value_t pin_get(const pin_t &p) { - return std::visit([](auto &&pp) -> value_t { - using P = std::decay_t; - if constexpr (std::is_same_v) { - // A port pin does have a scalar value: reading yields the - // queue reference (the port's value storage), writing is - // not allowed. Read through the sint accessor until the - // port change makes hal_get_port() available. - return (rtapi_sint)hal_get_sint(pp.handle()); - } else { - return pp.get(); - } - }, 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; - if constexpr (std::is_same_v) - throw std::invalid_argument("hal: cannot write to a port pin"); - else - pp.set(std::visit([](auto &&x) -> typename P::value_type { - return static_cast(x); - }, v)); + 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 { - using P = std::decay_t; - if constexpr (std::is_same_v) - return HAL_PORT; - else - return traits::type; + return traits::value_type>::type; }, p); } @@ -339,13 +268,6 @@ public: const std::string &name() const { return name_; } hal_type_t type() const { return detail::pin_type(p_); } - bool is_port() const { return std::holds_alternative(p_); } - - port &as_port() { - if(auto *pp = std::get_if(&p_)) - return *pp; - throw std::invalid_argument("hal::anypin: not a port pin"); - } value_t get() const { return detail::pin_get(p_); } void set(const value_t &v) { detail::pin_set(p_, v); } @@ -422,20 +344,6 @@ public: 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)); -#ifdef HALXX_WITH_QUERY_API - // Ports are created with hal_pin_new_port(), which is core HAL - // library but not present in the base getter/setter tree. - case HAL_PORT: { - 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 = hal_pin_new_port(id_, (hal_pin_dir_t)d, &u->s, "%s", fullname(name).c_str()); - if(rv) - throw std::runtime_error("hal::component: newpin(" + name + ") failed: " + detail::errstr(rv)); - items_.emplace(name, port(&u->s)); - return anypin(port(&u->s), fullname(name)); - } -#endif default: throw std::invalid_argument("hal::component: newpin(" + name + "): unsupported type"); } diff --git a/src/hal/halpybind.cc b/src/hal/halpybind.cc index 36cd050a1cc..54ff57bddd9 100644 --- a/src/hal/halpybind.cc +++ b/src/hal/halpybind.cc @@ -89,25 +89,7 @@ PYBIND11_MODULE(halpp, m) { .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) - .def("is_port", &halxx::anypin::is_port) - // HAL_PORT access. Binary data, exposed as bytes. These throw - // ValueError on non-port pins. - .def("write", [](halxx::anypin &p, py::bytes d) { - std::string s = d; - return p.as_port().write(s.data(), (unsigned)s.size()); - }) - .def("read", [](halxx::anypin &p, unsigned c) { - auto v = p.as_port().read(c); - return py::bytes(std::string(v.begin(), v.end())); - }) - .def("peek", [](halxx::anypin &p, unsigned c) { - auto v = p.as_port().peek(c); - return py::bytes(std::string(v.begin(), v.end())); - }) - .def("size", [](halxx::anypin &p) { return p.as_port().size(); }) - .def("writable", [](halxx::anypin &p) { return p.as_port().writable(); }) - .def("readable", [](halxx::anypin &p) { return p.as_port().readable(); }); + .def("get_name", &halxx::anypin::name); py::class_(m, "component") .def(py::init()) diff --git a/tests/halpp/smoke.py b/tests/halpp/smoke.py index 508cee5cc7d..87330b556cb 100644 --- a/tests/halpp/smoke.py +++ b/tests/halpp/smoke.py @@ -25,10 +25,13 @@ def check(cond, msg): 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: - p_port = h.newpin("port-out", halpp.HAL_PORT, halpp.HAL_OUT) + h.newpin("port-out", halpp.HAL_PORT, halpp.HAL_OUT) + check(False, "port pin creation raises until API break") except ValueError: - p_port = None # HAL_PORT creation requires the query API + check(True, "port pin creation raises until API break") # --- params --------------------------------------------------------------- pm = h.newparam("gain", halpp.HAL_FLOAT, halpp.HAL_RW) @@ -85,15 +88,6 @@ def check(cond, msg): else: print("note: query API not present, skipping by-name checks") -# --- port ------------------------------------------------------------------ -if p_port is not None: - check(p_port.is_port(), "port pin is_port") -try: - p_bit.as_port() - check(False, "non-port as_port raises") -except (ValueError, Exception): - check(True, "non-port as_port raises") - h.exit() if HAVE_QUERY: check(not halpp.component_exists("halpp-test"), "exit removes component")