From e5e59201b243e5d03f749da3098767078501dd52 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:53:43 +1000 Subject: [PATCH] hal: C++ API (hal.hh) and pybind11 bindings on the 64-bit HAL API Reintroduce a C++ interface for HAL, replacing the old hal.hh that was removed ahead of the API break. It is built strictly on the public C API and the 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, exported to include/. Typed pin/param handles via traits over rtapi_bool, rtapi_sint, rtapi_uint and rtapi_real (compile-time accessor selection, no 32-bit handles), a runtime-typed pin_t variant and anypin for name-based access, a component class with add_pin for the struct-member idiom, and ULAPI by-name query/set functions on hal_get_p/hal_set_p/hal_get_s/hal_set_s/hal_comp_by_name. 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; range errors are reported after the library releases the HAL mutex. - Streams: linuxcnc::hal::stream, a move-only wrapper around hal_stream_t, created with a depth and a typestring or attached to an existing key. Element types drive the conversion of samples in both directions with the same range checks as the by-name setters. Library failures reported as a negative errno are thrown as std::system_error. - halpybind.cc: pybind11 module (halpp.so) exposing component, Pin, stream and the by-name functions. Built alongside _hal/hal.py, replacing nothing. Type and direction tags are the _hal.Type/_hal.Dir IntEnum classes: arguments accept the members or plain ints, results come back as members. Importing _hal also initializes the HAL library. String set values go through setps_common_cb for halcmd-consistent parsing, std::system_error becomes OSError, and SIGTERM raises KeyboardInterrupt as it does in _hal. - Ports: linuxcnc::hal::port, a move-only handle for HAL_PORT pins with all-or-nothing read/peek/peek_commit/write plus readable/writable/size/clear, created with component::newport() or runtime-typed newpin(). The buffer belongs to the linking signal and is sized with set_signal(); an unlinked port has no buffer and its reads and writes fail quietly. A port pin has no scalar value: get_value() and the runtime-typed get/set raise for it, leaving hal_get_p()/hal_set_p() semantics on port pins to the library. The size is read from the signal (get_value() on a port signal) or with port::size(). halpp exposes the same calls on Pin, with bytes in and out (str is written as UTF-8). - tests/halpp: Python and native C++ smoke suites, plus a stream create/attach pair across two processes the way sampler and streamer are used. The C++ suite compiles against the tree, so the test is skipped for installed packages. Based on the pybind11 branch by rene-dev, rebuilt on the new HAL API. --- src/Makefile | 1 + src/hal/Submakefile | 11 + src/hal/hal.hh | 866 +++++++++++++++++++++++++++++++++++ src/hal/halpybind.cc | 269 +++++++++++ tests/halpp/README | 12 + tests/halpp/cpp_test.cc | 157 +++++++ tests/halpp/expected | 130 ++++++ tests/halpp/skip | 5 + tests/halpp/smoke.hal | 3 + tests/halpp/smoke.py | 243 ++++++++++ tests/halpp/stream_reader.py | 36 ++ tests/halpp/stream_writer.py | 29 ++ tests/halpp/test.sh | 14 + 13 files changed, 1776 insertions(+) create mode 100644 src/hal/hal.hh create mode 100644 src/hal/halpybind.cc create mode 100644 tests/halpp/README create mode 100644 tests/halpp/cpp_test.cc create mode 100644 tests/halpp/expected create mode 100755 tests/halpp/skip create mode 100644 tests/halpp/smoke.hal create mode 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/Makefile b/src/Makefile index c05d9071f95..9824ea93916 100644 --- a/src/Makefile +++ b/src/Makefile @@ -399,6 +399,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 8b7e42bac14..2c614cb1af8 100644 --- a/src/hal/Submakefile +++ b/src/hal/Submakefile @@ -24,5 +24,16 @@ $(HALMODULE): $(call TOOBJS, $(HALMODULESRCS)) $(HALLIB) $(ECHO) Linking python module $(notdir $@) $(Q)$(CXX) $(LDFLAGS) -shared -o $@ $^ -lfmt +# pybind11 C++ bindings on hal.hh +HALPPSRCS := hal/halpybind.cc hal/setps_util.c +PYSRCS += $(HALPPSRCS) + +HALPP := ../lib/python/halpp.so +$(HALPP): $(call TOOBJS, $(HALPPSRCS)) $(HALLIB) + $(ECHO) Linking python module $(notdir $@) + $(Q)$(CXX) $(LDFLAGS) -shared -o $@ $^ + +PYTARGETS += $(HALPP) + TARGETS += $(HALLIB) ../lib/liblinuxcnchal.so.0 PYTARGETS += $(HALMODULE) diff --git a/src/hal/hal.hh b/src/hal/hal.hh new file mode 100644 index 00000000000..c38090343a0 --- /dev/null +++ b/src/hal/hal.hh @@ -0,0 +1,866 @@ +/* + 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. + + HAL integers are 64-bit only (HAL_SINT, HAL_UINT); there are no + 32-bit handles. The by-name query/set section is built on the HAL + query API and is user-space only. + + HAL_PORT pins get their own handle, port, with byte-stream access + instead of a scalar value. +*/ +#ifndef HALXX_HH +#define HALXX_HH + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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, +}; + +// 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; + +//---------------------------------------------------------------------- +// 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. +//---------------------------------------------------------------------- +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_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 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_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()); + } +}; + +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; +}; + +//---------------------------------------------------------------------- +// port - handle of a HAL_PORT pin, an asynchronous one-way byte stream +// with one reader (the IN pin) and one writer (the OUT pin). The +// buffer belongs to the signal the pins are linked to and is sized by +// setting that signal ("sets"), see set_signal(); an unlinked port has +// no buffer and reads and writes fail. Like pin, the handle re-reads +// the slot on every access. Reads and writes are all or nothing. +//---------------------------------------------------------------------- +class port { +public: + port() = default; + explicit port(hal_port_t *slot) : slot_(slot) {} + port(const port &) = delete; + port &operator=(const port &) = delete; + port(port &&) = default; + port &operator=(port &&) = default; + + // Explicit second handle to the same HAL object. + port dup() const { return port(slot_); } + + // False while the pin is not linked to a sized port signal. + bool has_buffer() const { return 0 != hal_get_port(handle()); } + + unsigned size() const { return has_buffer() ? hal_port_buffer_size(handle()) : 0; } + unsigned readable() const { return has_buffer() ? hal_port_readable(handle()) : 0; } + unsigned writable() const { return has_buffer() ? hal_port_writable(handle()) : 0; } + void clear() const { if(has_buffer()) hal_port_clear(handle()); } + + bool read(char *dst, unsigned n) const { return has_buffer() && hal_port_read(handle(), dst, n); } + bool peek(char *dst, unsigned n) const { return has_buffer() && hal_port_peek(handle(), dst, n); } + bool peek_commit(unsigned n) const { return has_buffer() && hal_port_peek_commit(handle(), n); } + bool write(const char *src, unsigned n) const { return has_buffer() && hal_port_write(handle(), src, n); } + + // n bytes, or nothing when fewer than n are readable. + std::optional> read(unsigned n) const { + std::vector buf(n); + if(!read(buf.data(), n)) + return std::nullopt; + return buf; + } + std::optional> peek(unsigned n) const { + std::vector buf(n); + if(!peek(buf.data(), n)) + return std::nullopt; + return buf; + } + bool write(const std::vector &data) const { + return write(data.data(), (unsigned)data.size()); + } + + hal_port_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"); + } + hal_port_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, port>; + +namespace detail { + +// In-place scalar access on a runtime-typed item. A port pin has no +// scalar value: use the port calls, the buffer size belongs to the +// linking signal. +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: a port pin has no value, use the port calls"); + else + return pp.get(); + }, p); +} + +// Convert a runtime value to the value type of a typed handle. Throws +// std::out_of_range instead of truncating or wrapping. +template +T checked_cast(X x) +{ + long double xv = static_cast(x); + if constexpr(std::is_same_v) { + if(xv < (long double)RTAPI_SINT_MIN || xv > (long double)RTAPI_SINT_MAX) + throw std::out_of_range("hal: value does not fit a sint item"); + } else if constexpr(std::is_same_v) { + if(xv < 0 || xv > (long double)RTAPI_UINT_MAX) + throw std::out_of_range("hal: value does not fit a uint item"); + } + return static_cast(x); +} + +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: a port pin has no value, use the port calls"); + } else { + pp.set(std::visit([](auto &&x) -> typename P::value_type { + return checked_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_); } + + value_t get() const { return detail::pin_get(p_); } + void set(const value_t &v) { detail::pin_set(p_, v); } + + bool is_port() const { return std::holds_alternative(p_); } + const port &as_port() const { + if(const port *pp = std::get_if(&p_)) + return *pp; + throw std::invalid_argument("hal: " + name_ + " is not a port pin"); + } + +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: " + hal_strerror(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; } + const std::string &getprefix() const { return prefix_; } + + void ready() { + int rv = hal_ready(id_); + if(rv) + throw std::runtime_error(std::string("hal::component: hal_ready failed: ") + hal_strerror(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: " + hal_strerror(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); + } + + // Create a port pin ".". A port is IN (reader) or OUT + // (writer); there are no port params. + port newport(const std::string &name, dir d) { + hal_refs_u *u = (hal_refs_u *)hal_malloc(sizeof(*u)); + if(!u) + throw std::runtime_error("hal::component: newport(" + name + "): hal_malloc failed"); + int rv = hal_pin_new_port(id_, (hal_pdir_t)d, &u->p, "%s", fullname(name).c_str()); + if(rv) + throw std::runtime_error("hal::component: newport(" + name + ") failed: " + hal_strerror(rv)); + items_.emplace(name, port(&u->p)); + return port(&u->p); + } + + void add_port(const std::string &name, dir d, port &target) { + target = newport(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_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)); + case HAL_PORT: return anypin(pin_t(newport(name, d)), fullname(name)); + default: + throw std::invalid_argument("hal::component: newpin(" + name + "): unsupported type"); + } + } + + // Create a typed parameter ".". + template + 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: " + hal_strerror(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_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"); + } + } + + // 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 + 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_; +}; + +//---------------------------------------------------------------------- +// 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_SINT: return 's'; + case HAL_UINT: return 'u'; + 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_SINT: return (rtapi_sint)d.s; + case HAL_UINT: return (rtapi_uint)d.u; + 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_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->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). +//---------------------------------------------------------------------- +#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. +//---------------------------------------------------------------------- +#ifdef ULAPI + +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_BOOL: + out->b = (0 != 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; + case HAL_PORT: + // Buffer size of a port signal; the library refuses it for pins. + if(xv < 1 || xv > HAL_PORT_SIZE_MAX) { ok = false; break; } + out->u = 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_SINT: return (rtapi_sint)v.s; + case HAL_UINT: return (rtapi_uint)v.u; + case HAL_REAL: return (rtapi_real)v.r; + case HAL_PORT: return (rtapi_uint)v.u; + default: + throw std::invalid_argument("hal: item has an 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; +} + +} // 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. A port signal +// reads as its buffer size; a port pin has no value. Throws +// std::invalid_argument if the lookup fails or the item is a port pin. +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) { + if(HAL_PORT == q.pp.type) + throw std::invalid_argument("hal: get_value(" + name + "): a port pin has no value, read the size from its signal"); + 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: " + hal_strerror(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: " + hal_strerror(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: " + hal_strerror(rv)); +} + +#endif // ULAPI + +} // namespace hal +} // namespace linuxcnc + +#endif // HALXX_HH diff --git a/src/hal/halpybind.cc b/src/hal/halpybind.cc new file mode 100644 index 00000000000..b3f2396d8c5 --- /dev/null +++ b/src/hal/halpybind.cc @@ -0,0 +1,269 @@ +/* + 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 + stream - sample FIFO shared with sampler/streamer + module fns - by-name get/set, signals, component queries +*/ +#include +#include + +#include +#include + +#include "hal.hh" +#include "setps_util.h" + +namespace py = pybind11; +namespace halxx = linuxcnc::hal; + +// The IntEnum classes _hal.Type and _hal.Dir, fetched at import. The +// references are leaked on purpose, as in halquery.cc, so no py::object +// destructor runs during interpreter teardown. +static py::object halenumtype; +static py::object halenumdir; + +namespace pybind11 { namespace detail { + +// Casts between the native enum values and the shared IntEnum classes +// registered by _hal, built from the hal.h constants. 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) { + return halenumtype(static_cast(v)).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) { + return halenumdir(static_cast(v)).release(); + } +}; + +}} // namespace pybind11::detail + +// 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)); +} + +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())); + } + }); + + // Importing _hal initializes the user-land HAL library, so the + // by-name query functions work without a component, and provides + // the IntEnum type and direction tags. + py::module_ halmod = py::module_::import("_hal"); + halenumtype = halmod.attr("Type"); + halenumdir = halmod.attr("Dir"); + halenumtype.inc_ref(); + halenumdir.inc_ref(); + m.attr("Type") = halenumtype; + m.attr("Dir") = halenumdir; + + // 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); + + // 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_property_readonly("type", &halxx::anypin::type) + .def("get_name", &halxx::anypin::name) + // Port pins: byte-stream access, all or nothing, as in the _hal + // module. read()/peek() return None when fewer than n bytes are + // readable; the other calls raise ValueError on a non-port pin. + .def("read", [](const halxx::anypin &p, unsigned n) -> py::object { + auto data = p.as_port().read(n); + if(!data) + return py::none(); + return py::bytes(data->data(), data->size()); + }, py::arg("n")) + .def("peek", [](const halxx::anypin &p, unsigned n) -> py::object { + auto data = p.as_port().peek(n); + if(!data) + return py::none(); + return py::bytes(data->data(), data->size()); + }, py::arg("n")) + .def("peek_commit", [](const halxx::anypin &p, unsigned n) { + return p.as_port().peek_commit(n); + }, py::arg("n")) + // str is written as its UTF-8 encoding. + .def("write", [](const halxx::anypin &p, const std::string &data) { + return p.as_port().write(data.data(), (unsigned)data.size()); + }, py::arg("data")) + .def("readable", [](const halxx::anypin &p) { return p.as_port().readable(); }) + .def("writable", [](const halxx::anypin &p) { return p.as_port().writable(); }) + .def("size", [](const halxx::anypin &p) { return p.as_port().size(); }) + .def("clear", [](const halxx::anypin &p) { p.as_port().clear(); }); + + 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("__contains__", &halxx::component::contains) + .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); + }); + + // '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")); +} diff --git a/tests/halpp/README b/tests/halpp/README new file mode 100644 index 00000000000..dbee28932a8 --- /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, port pins, 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 testing installed packages, where no tree is present. diff --git a/tests/halpp/cpp_test.cc b/tests/halpp/cpp_test.cc new file mode 100644 index 00000000000..24626a43ebc --- /dev/null +++ b/tests/halpp/cpp_test.cc @@ -0,0 +1,157 @@ +// 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); + + // Port pins: separate handle type, byte-stream access + auto pout = c.newport("pout", hal::dir::OUT); + hal::port pin_in; + c.add_port("pin", hal::dir::IN, pin_in); + + // 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 negative sint"); + c.setitem("in", -((rtapi_sint)1 << 40)); + CHECK(std::get(c.getitem("in")) == -((rtapi_sint)1 << 40), "setitem/getitem 64-bit sint"); + CHECK(std::get(c.getitem("gain")) == 1.5, "getitem param double"); + { + bool threw = false; + try { c.setitem("count", -1); } catch(const std::out_of_range &) { threw = true; } + CHECK(threw && cnt.get() == ((rtapi_uint)1 << 60), "setitem range check throws, value kept"); + } + CHECK(c.contains("flag"), "contains()"); + + c.ready(); + + // Signals + CHECK(hal::signal_new("halcpp-sig", HAL_SINT) == 0, "signal_new"); + CHECK(hal::link("halcpp-test.in", "halcpp-sig") == 0, "link"); + + 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", -1); } 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"); + + // Ports: the linking signal owns the buffer, "sets" sizes it + CHECK(!pout.has_buffer() && !pout.write("x", 1), "unlinked port has no buffer"); + CHECK(hal::signal_new("halcpp-port", HAL_PORT) == 0, "port signal_new"); + CHECK(hal::link("halcpp-test.pout", "halcpp-port") == 0, "link port writer"); + CHECK(hal::link("halcpp-test.pin", "halcpp-port") == 0, "link port reader"); + hal::set_signal("halcpp-port", 8); + CHECK(pout.size() == 8 && pin_in.size() == 8, "port buffer sized by set_signal"); + CHECK(std::get(hal::get_value("halcpp-port")) == 8, "port signal reads as its buffer size"); + threw = false; + try { hal::get_value("halcpp-test.pin"); } catch(const std::invalid_argument &) { threw = true; } + CHECK(threw, "port pin by name has no value"); + threw = false; + try { c.getitem("pin"); } catch(const std::invalid_argument &) { threw = true; } + CHECK(threw, "getitem on a port pin throws"); + CHECK(pout.write(std::vector{'a', 'b', 'c'}), "port write"); + CHECK(pin_in.readable() == 3, "port readable"); + auto got = pin_in.read(3); + CHECK(got && std::string(got->begin(), got->end()) == "abc", "port read"); + CHECK(!pin_in.read(1).has_value(), "read of an empty port is empty"); + threw = false; + try { c.setitem("pin", 1); } catch(const std::invalid_argument &) { threw = true; } + CHECK(threw, "setitem on a port pin throws"); + + // 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_SINT, "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_sint)i, (rtapi_uint)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)0, (rtapi_sint)-1}); } + 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(); + CHECK(!hal::component_exists("halcpp-test"), "exit removes component"); + } 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/expected b/tests/halpp/expected new file mode 100644 index 00000000000..386915a6d08 --- /dev/null +++ b/tests/halpp/expected @@ -0,0 +1,130 @@ +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 - newpin accepts plain ints for the tags +ok - port pin type +ok - scalar pin type +ok - port param raises +ok - param set/get roundtrip +ok - bool pin set/get +ok - sint pin set/get (negative) +ok - sint pin set/get (64-bit) +ok - uint pin set/get (64-bit) +ok - negative value into a uint pin raises +ok - comp __setitem__/__getitem__ real +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 - set_value/get_value sint beyond 32 bits +ok - set_value from text +ok - negative value into uint raises +ok - unlinked port has no buffer +ok - write to an unlinked port fails +ok - read from an unlinked port returns None +ok - port signal_new +ok - link port writer +ok - link port reader +ok - port signal reads as its buffer size +ok - port pin by name has no value +ok - port pin handle has no value +ok - port size() reads the signal's buffer size +ok - resizing a port signal raises +ok - setp on a port pin raises +ok - port write bytes +ok - port write str as UTF-8 +ok - readable counts the bytes written +ok - writable is what is left +ok - peek +ok - peek consumes nothing +ok - peek_commit +ok - read +ok - read of an empty port returns None +ok - write beyond the buffer fails +ok - a failed write writes nothing +ok - clear empties the port +ok - set on a port pin raises +ok - port call on a scalar pin raises +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 +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 negative sint +ok - setitem/getitem 64-bit sint +ok - getitem param double +ok - setitem range check throws, value kept +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 - unlinked port has no buffer +ok - port signal_new +ok - link port writer +ok - link port reader +ok - port buffer sized by set_signal +ok - port signal reads as its buffer size +ok - port pin by name has no value +ok - getitem on a port pin throws +ok - port write +ok - port readable +ok - port read +ok - read of an empty port is empty +ok - setitem on a port pin throws +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..088ae2947dc --- /dev/null +++ b/tests/halpp/skip @@ -0,0 +1,5 @@ +#!/bin/sh +# test.sh compiles cpp_test.cc against the tree headers and library, which +# are only available in run-in-place builds. Skip when testing installed +# packages. +[ -z "$SYSTEM_BUILD" ] 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 new file mode 100755 index 00000000000..c71c92644bf --- /dev/null +++ b/tests/halpp/smoke.py @@ -0,0 +1,243 @@ +#!/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 + +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") + +# --- shared type/dir tagging enums from _hal ------------------------------ +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_bool = h.newpin("bool-out", halpp.Type.BOOL, halpp.Dir.OUT) +p_real = h.newpin("real-in", halpp.Type.REAL, halpp.Dir.IN) +p_sint = h.newpin("sint-io", halpp.Type.SINT, halpp.Dir.IO) +p_uint = h.newpin("uint-io", halpp.Type.UINT, halpp.Dir.IO) +check(h.newpin("int-tag", int(halpp.Type.REAL), int(halpp.Dir.IN)).name == "halpp-test.int-tag", + "newpin accepts plain ints for the tags") + +p_pout = h.newpin("port-out", halpp.Type.PORT, halpp.Dir.OUT) +p_pin = h.newpin("port-in", halpp.Type.PORT, halpp.Dir.IN) +check(p_pout.type is halpp.Type.PORT, "port pin type") +check(p_bool.type is halpp.Type.BOOL, "scalar pin type") +try: + h.newparam("port-param", halpp.Type.PORT, halpp.Dir.RW) + check(False, "port param raises") +except ValueError: + check(True, "port param raises") + +# --- params --------------------------------------------------------------- +pm = h.newparam("gain", halpp.Type.REAL, halpp.Dir.RW) +pm.set(2.5) +check(abs(pm.get() - 2.5) < 1e-9, "param set/get roundtrip") + +# --- pin set/get via handle ---------------------------------------------- +p_bool.set(True) +check(p_bool.get() == True, "bool pin set/get") +p_sint.set(-12345) +check(p_sint.get() == -12345, "sint pin set/get (negative)") +p_sint.set(-2**62) +check(p_sint.get() == -2**62, "sint pin set/get (64-bit)") +p_uint.set(2**63 + 5) +check(p_uint.get() == 2**63 + 5, "uint pin set/get (64-bit)") +try: + p_uint.set(-1) + check(False, "negative value into a uint pin raises") +except IndexError: + check(p_uint.get() == 2**63 + 5, "negative value into a uint pin raises") + +# --- component item access ------------------------------------------------ +h["real-in"] = 3.25 +check(abs(h["real-in"] - 3.25) < 1e-9, "comp __setitem__/__getitem__ real") +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.Type.UINT, halpp.Dir.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.Type.REAL) == 0, "signal_new") +check(halpp.link("halpp-test.real-in", "halpp-sig") == 0, "link") + +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.real-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.real-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") +halpp.set_value("halpp-test.sint-io", 2**40) +check(halpp.get_value("halpp-test.sint-io") == 2**40, "set_value/get_value sint beyond 32 bits") +halpp.set_value("halpp-test.uint-io", "12345678901") +check(halpp.get_value("halpp-test.uint-io") == 12345678901, "set_value from text") +try: + halpp.set_value("halpp-test.uint-io", -1) + check(False, "negative value into uint raises") +except IndexError: + check(True, "negative value into uint raises") + +# --- ports ----------------------------------------------------------------- +# The buffer belongs to the signal: unlinked ports have none, "sets" on +# the port signal allocates it. +check(p_pout.size() == 0 and p_pin.readable() == 0, "unlinked port has no buffer") +check(p_pout.write(b"x") == False, "write to an unlinked port fails") +check(p_pin.read(1) is None, "read from an unlinked port returns None") +check(halpp.signal_new("halpp-port", halpp.Type.PORT) == 0, "port signal_new") +check(halpp.link("halpp-test.port-out", "halpp-port") == 0, "link port writer") +check(halpp.link("halpp-test.port-in", "halpp-port") == 0, "link port reader") +halpp.set_signal("halpp-port", 16) +check(halpp.get_value("halpp-port") == 16, "port signal reads as its buffer size") +try: + halpp.get_value("halpp-test.port-in") + check(False, "port pin by name has no value") +except ValueError: + check(True, "port pin by name has no value") +try: + p_pin.get() + check(False, "port pin handle has no value") +except ValueError: + check(True, "port pin handle has no value") +check(p_pin.size() == 16 and p_pout.size() == 16, "port size() reads the signal's buffer size") +try: + halpp.set_signal("halpp-port", 32) + check(False, "resizing a port signal raises") +except ValueError: + check(True, "resizing a port signal raises") +try: + halpp.set_value("halpp-test.port-in", 8) + check(False, "setp on a port pin raises") +except ValueError: + check(True, "setp on a port pin raises") + +check(p_pout.write(b"hello") == True, "port write bytes") +check(p_pout.write("w\u00f6rld") == True, "port write str as UTF-8") +check(p_pin.readable() == 11, "readable counts the bytes written") +check(p_pout.writable() == 16 - 1 - 11, "writable is what is left") +check(p_pin.peek(5) == b"hello", "peek") +check(p_pin.readable() == 11, "peek consumes nothing") +check(p_pin.peek_commit(5) == True, "peek_commit") +check(p_pin.read(6) == "w\u00f6rld".encode(), "read") +check(p_pin.read(1) is None, "read of an empty port returns None") +check(p_pout.write(b"x" * 16) == False, "write beyond the buffer fails") +check(p_pin.readable() == 0, "a failed write writes nothing") +p_pout.write(b"abc") +p_pin.clear() +check(p_pin.readable() == 0, "clear empties the port") +try: + p_pin.set(1) + check(False, "set on a port pin raises") +except ValueError: + check(True, "set on a port pin raises") +try: + p_bool.read(1) + check(False, "port call on a scalar pin raises") +except ValueError: + check(True, "port call on a scalar pin raises") + +# --- 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.Type.REAL, "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") + +# 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, 0, -1)) + 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() +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") 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..220e703e1a9 --- /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"$HEADERS" \ + cpp_test.cc -o "$bindir/cpp_test" \ + -L"$LIBDIR" -Wl,-rpath,"$LIBDIR" -llinuxcnchal || exit 1 +halrun -I <