diff --git a/docs/src/config/python-hal-interface.adoc b/docs/src/config/python-hal-interface.adoc index 132a6cc6b7d..6d3e18047d0 100644 --- a/docs/src/config/python-hal-interface.adoc +++ b/docs/src/config/python-hal-interface.adoc @@ -804,29 +804,6 @@ hal.query.threads() -> dict:: ---- See xref:halquerythread[`hal.query.thread()`] for description of thread dictionary details. -== Class `hal.shm` -The `shm` class is an interface to create and manage shared memory segments. - -The `shm` class **should be considered __experimental__** and may or may not work as intended. -[WARNING] -==== -Do not rely on this class. -It may be removed in future releases. -==== - -=== shm methods - -shm = hal.shm(_comp_:object, _key_:int, _size_:int):: -Allocate a _size_ sized shared memory segment with ID _key_. - -shm.getbuffer():: -Returns a Python `memoryview` object of the shared memory segment. - -shm.setsize():: -This is non-functional. -**Do not use.** -You cannot increase or decrease the shared memory segment's size once it is created. - == HAL Port pipes A HAL port is a byte-oriented pipe that streams bytes from the writer to the reader. It may be used to transport data between real-time and non-real-time in either direction. diff --git a/include/hal.hh b/include/hal.hh new file mode 100644 index 00000000000..f70769e2974 --- /dev/null +++ b/include/hal.hh @@ -0,0 +1,990 @@ +/* + 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. + + Errors are exceptions, documented here once: + std::system_error a library call failed, with its errno + std::out_of_range a value does not fit its item, or there is + no item of that name or index + std::invalid_argument the wrong kind of item, e.g. a value asked + of a port pin, or an unsupported type + std::logic_error use of an uninitialized or closed handle + std::bad_alloc hal_malloc() failed + A lookup that finds nothing is not an error: it returns false or an + empty std::optional. Nothing throws out of a destructor or out of a + query callback, where unwinding would leave the HAL mutex held. +*/ +#ifndef HALXX_HH +#define HALXX_HH + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hal.h" + +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. +static 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 T exactly, or fail: nothing is rounded, +// truncated or wrapped. An integer type takes an integer in its range, +// or a real that is finite, whole and in range. A bool takes any value, +// non-zero is true, as in C. A real takes any value. Must not throw: the +// by-name setters call it with the HAL mutex held. +template +static inline bool convert(const value_t &v, T *out) noexcept +{ + return std::visit([out](auto x) -> bool { + using X = decltype(x); + if constexpr(std::is_same_v) { + *out = static_cast(x); + return true; + } else if constexpr(std::is_same_v) { + *out = (0 != x); + return true; + } else if constexpr(std::is_same_v) { + // 2^63 and 2^64 are exact as doubles, the integer limits are not + if(!std::isfinite(x) || x != std::trunc(x)) + return false; + if constexpr(std::is_same_v) { + // cppcheck-suppress incorrectLogicOperator + if(x < -0x1p63 || x >= 0x1p63) + return false; + } else if(x < 0 || x >= 0x1p64) + return false; + *out = static_cast(x); + return true; + } else if constexpr(std::is_same_v) { + if constexpr(std::is_same_v) + if(x > (rtapi_uint)RTAPI_SINT_MAX) + return false; + *out = static_cast(x); + return true; + } else { + if constexpr(std::is_same_v) + if(x < 0) + return false; + *out = static_cast(x); + return true; + } + }, v); +} + +// Same, for a typed handle: throws std::out_of_range on failure. +template +static inline T checked_cast(const value_t &v) +{ + T out{}; + if(!convert(v, &out)) + throw std::out_of_range(std::string("hal: value does not fit a ") + + (traits::type == HAL_BOOL ? "bool" : + traits::type == HAL_SINT ? "sint" : + traits::type == HAL_UINT ? "uint" : "real") + " item"); + return out; +} + +static 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(checked_cast(v)); + } + }, p); +} + +static 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, bool is_pin, dir d) + : p_(std::move(p)), name_(std::move(name)), is_pin_(is_pin), dir_(d) {} + anypin(const anypin &) = delete; + anypin &operator=(const anypin &) = delete; + anypin(anypin &&) = default; + anypin &operator=(anypin &&) = default; + + // Explicit second handle to the same HAL object. + anypin dup() const { + return anypin(std::visit([](auto &&pp) -> pin_t { return pp.dup(); }, p_), + name_, is_pin_, dir_); + } + + const std::string &name() const { return name_; } + + hal_type_t type() const { return detail::pin_type(p_); } + bool is_pin() const { return is_pin_; } + dir direction() const { return dir_; } + + 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_; + bool is_pin_ = true; + dir dir_ = dir::IN; +}; + +//---------------------------------------------------------------------- +// component - a userspace HAL component. Owns the comp_id and keeps a +// name-keyed map of its pins and params. +// +// A failing library call throws std::system_error carrying the errno +// the library returned; running out of HAL memory throws +// std::bad_alloc. +//---------------------------------------------------------------------- +class component { +public: + explicit component(const std::string &name) : prefix_(name) { + id_ = hal_init(name.c_str()); + if(id_ < 0) + throw std::system_error(-id_, std::generic_category(), + "hal::component: hal_init(" + name + ") failed"); + } + 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::system_error(-rv, std::generic_category(), "hal::component: hal_ready failed"); + } + + // Back to not ready, to add pins and params; call ready() again after. + void unready() { + int rv = hal_unready(id_); + if(rv) + throw std::system_error(-rv, std::generic_category(), "hal::component: hal_unready failed"); + } + + 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::bad_alloc(); + int rv = traits::new_pin(id_, (hal_pdir_t)d, traits::slot(u), def, fullname(name)); + if(rv) + throw std::system_error(-rv, std::generic_category(), "hal::component: newpin(" + name + ") failed"); + items_.emplace(name, anypin(pin(traits::slot(u)), fullname(name), true, d)); + 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::bad_alloc(); + int rv = hal_pin_new_port(id_, (hal_pdir_t)d, &u->p, "%s", fullname(name).c_str()); + if(rv) + throw std::system_error(-rv, std::generic_category(), "hal::component: newport(" + name + ") failed"); + items_.emplace(name, anypin(port(&u->p), fullname(name), true, d)); + 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: newpin(name, d); break; + case HAL_SINT: newpin(name, d); break; + case HAL_UINT: newpin(name, d); break; + case HAL_REAL: newpin(name, d); break; + case HAL_PORT: newport(name, d); break; + default: + throw std::invalid_argument("hal::component: newpin(" + name + "): unsupported type"); + } + return item(name).dup(); + } + + // 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::bad_alloc(); + int rv = traits::new_param(id_, (hal_pdir_t)d, traits::slot(u), def, fullname(name)); + if(rv) + throw std::system_error(-rv, std::generic_category(), "hal::component: newparam(" + name + ") failed"); + items_.emplace(name, anypin(pin(traits::slot(u)), fullname(name), false, d)); + return pin(traits::slot(u)); + } + + anypin newparam(const std::string &name, hal_type_t type, dir d) { + switch(type) { + case HAL_BOOL: newparam(name, d); break; + case HAL_SINT: newparam(name, d); break; + case HAL_UINT: newparam(name, d); break; + case HAL_REAL: newparam(name, d); break; + default: + throw std::invalid_argument("hal::component: newparam(" + name + "): unsupported type"); + } + return item(name).dup(); + } + + // Item access by short name. Pins and params share one namespace, + // HAL refuses a pin and a param of the same name. + value_t getitem(const std::string &name) const { return item(name).get(); } + + template + void setitem(const std::string &name, T value) { item(name).set(value_t(value)); } + + bool contains(const std::string &name) const { return items_.count(name); } + + // True for a pin, false for a param. Throws std::out_of_range when + // there is no item of that name. + bool is_pin(const std::string &name) const { return item(name).is_pin(); } + + anypin &item(const std::string &name) { + if(auto it = items_.find(name); it != items_.end()) + return it->second; + throw std::out_of_range("hal::component: no pin or param '" + name + "'"); + } + const anypin &item(const std::string &name) const { + return const_cast(this)->item(name); + } + + // All pins and params, by short name. + const std::map &items() const { return items_; } + +private: + std::string fullname(const std::string &n) const { return prefix_ + "." + n; } + + int id_ = -1; + std::string prefix_; + std::map items_; +}; + +//---------------------------------------------------------------------- +// 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(). +static 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 '?'; + } +} + +static 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"); + } +} + +// Convert a runtime value into a stream element of the given type, see +// convert(). Returns false when it does not fit; the only caller, +// stream::write(), then throws before anything is written. +static inline bool convert_stream_value(hal_type_t target, const value_t &v, hal_stream_data_u *out) +{ + switch(target) { + case HAL_BOOL: { rtapi_bool b; if(!convert(v, &b)) return false; out->b = b; return true; } + case HAL_SINT: return convert(v, &out->s); + case HAL_UINT: return convert(v, &out->u); + case HAL_REAL: return convert(v, &out->f); + default: return false; + } +} + +} // 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 +static inline int signal_new(const std::string &name, hal_type_t type) +{ + return hal_signal_new(name.c_str(), type); +} +static inline int link(const std::string &pin_name, const std::string &sig_name) +{ + return hal_link(pin_name.c_str(), sig_name.c_str()); +} +static inline int unlink(const std::string &pin_name) +{ + return hal_unlink(pin_name.c_str()); +} +static 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, see convert(). +// 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 when the +// value does not fit, the caller reports it after the library call. +static inline bool convert_value(hal_type_t target, const value_t &v, hal_query_value_u *out) +{ + switch(target) { + case HAL_BOOL: { rtapi_bool b; if(!convert(v, &b)) return false; out->b = b; return true; } + case HAL_SINT: return convert(v, &out->s); + case HAL_UINT: return convert(v, &out->u); + case HAL_REAL: return convert(v, &out->r); + case HAL_PORT: { + // Buffer size of a port signal; the library refuses it for pins. + rtapi_uint n; + if(!convert(v, &n) || n < 1 || n > HAL_PORT_SIZE_MAX) + return false; + out->u = n; + return true; + } + default: + return false; + } +} + +static 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; +}; +static 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; +} +static 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 + +namespace detail { + +// A lookup that finds nothing answers the question with false; any +// other failure (HAL not mapped, a bad argument) is not an answer and +// throws std::system_error with the library's errno. +static inline bool found(int rv, const char *what, const std::string &name) +{ + if(0 == rv) + return true; + if(-ENOENT == rv) + return false; + throw std::system_error(-rv, std::generic_category(), + std::string("hal: ") + what + "(" + name + ") failed"); +} + +} // namespace detail + +// The component's state by name or by id, nothing when there is no such +// component. Throws std::system_error when the lookup fails otherwise. +static inline std::optional component_query(const std::string &name) +{ + hal_query_t q = {}; + if(!detail::found(hal_comp_by_name(name.c_str(), &q), "hal_comp_by_name", name)) + return std::nullopt; + return q; +} +static inline std::optional component_query(int comp_id) +{ + hal_query_t q = {}; + if(!detail::found(hal_comp_by_id(comp_id, &q), "hal_comp_by_id", std::to_string(comp_id))) + return std::nullopt; + return q; +} + +// True if the component is loaded. +static inline bool component_exists(const std::string &name) { return component_query(name).has_value(); } +static inline bool component_exists(int comp_id) { return component_query(comp_id).has_value(); } + +// True if the component is loaded and has called hal_ready(). +static inline bool component_is_ready(const std::string &name) +{ + auto q = component_query(name); + return q && q->comp.ready; +} +static inline bool component_is_ready(int comp_id) +{ + auto q = component_query(comp_id); + return q && q->comp.ready; +} + +// True if the pin exists, is connected to a signal, and that signal +// has at least one writer. +static inline bool pin_has_writer(const std::string &name) +{ + hal_query_t q = {}; + q.name = name.c_str(); + q.qtype = HAL_QTYPE_PIN; + if(!detail::found(hal_getref_p(&q), "hal_getref_p", name) || !q.pp.signal) + return false; + hal_query_t sq = {}; + sq.name = q.pp.signal; + if(!detail::found(hal_getref_s(&sq), "hal_getref_s", sq.name)) + 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::system_error if there is no such item or the lookup fails, and +// std::invalid_argument for a port pin. +static 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::system_error(-rv, std::generic_category(), "hal: get_value(" + name + ") failed"); +} + +// The state of a pin or param by name, nothing when there is none. +// 'qtype' HAL_QTYPE_PIN or HAL_QTYPE_PARAM limits the search. pp.ref +// reads the value with the hal_get_* accessors; for a connected pin it +// is the signal's. The value union is not filled in, and the pointers +// in the result point into HAL shared memory. Throws std::system_error +// when the lookup fails otherwise. +static inline std::optional pin_param_query(const std::string &name, + hal_qtype_t qtype = HAL_QTYPE_ANY) +{ + hal_query_t q = {}; + q.name = name.c_str(); + q.qtype = qtype; + if(!detail::found(hal_getref_p(&q), "hal_getref_p", name)) + return std::nullopt; + return q; +} + +// The state of a signal by name, nothing when there is none; sig.ref +// reads the value, as for pin_param_query(). +static inline std::optional signal_query(const std::string &name) +{ + hal_query_t q = {}; + q.name = name.c_str(); + if(!detail::found(hal_getref_s(&q), "hal_getref_s", name)) + return std::nullopt; + return q; +} + +// Set a pin or param by name ("setp"). The value is coerced to the +// item's actual HAL type with range checks; std::out_of_range when it +// does not fit, std::system_error when the library refuses the set. +static 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::system_error(-rv, std::generic_category(), "hal: set_value(" + name + ") failed"); +} + +// Set a signal by name ("sets"), with the same failures. +static 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::system_error(-rv, std::generic_category(), "hal: set_signal(" + name + ") failed"); +} + +#endif // ULAPI + +} // namespace hal +} // namespace linuxcnc + +#endif // HALXX_HH diff --git a/src/hal/Submakefile b/src/hal/Submakefile index 8b7e42bac14..3d9a455f5c0 100644 --- a/src/hal/Submakefile +++ b/src/hal/Submakefile @@ -16,7 +16,7 @@ $(HALLIB).0: $(call TOOBJS, $(HALLIBSRCS)) @rm -f $@ $(Q)$(CC) $(LDFLAGS) -Wl,-soname,$(notdir $@) -shared -o $@ $^ $(HALLIB_LIBS) $(ULAPI_LDFLAGS) -HALMODULESRCS := hal/halmodule.cc hal/setps_util.c hal/halquery.cc +HALMODULESRCS := hal/halmodule.cc hal/halquery.cc PYSRCS += $(HALMODULESRCS) HALMODULE := ../lib/python/_hal.so diff --git a/src/hal/halmodule.cc b/src/hal/halmodule.cc index 9510653fe7c..9882786989d 100644 --- a/src/hal/halmodule.cc +++ b/src/hal/halmodule.cc @@ -17,116 +17,119 @@ // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -#include -#include +// +// The _hal Python module, wrapped by lib/python/hal.py. A pybind11 binding +// of the C++ HAL interface in hal.hh. +// +// Python values are converted to HAL values here, before any HAL call, by +// the lenient rules this module always had (a float into a bool, "yes" +// into a bool, int() truncation into the integer types, ...). The result +// is exact for its HAL type, so hal.hh's strict conversion passes it on +// unchanged, and no Python code runs while the library holds its mutex. +// +#include + +#include +#include +#include +#include #include -#include -#include -#include +#include #include -#include +#include #include "halqrec.hh" -#define EXCEPTION_IF_NOT_LIVE(retval) do { \ - if(self->hal_id <= 0) { \ - PyErr_SetString(PyExc_RuntimeError, "Invalid operation on closed HAL component"); \ - return retval; \ - } \ - } while(0) - -// Use stdint interfaces when we have them -#if PY_VERSION_HEX >= 0x030e00f0 // 3.14 -#define PyLong_FromRtapiS32 PyLong_FromInt32 -#define PyLong_FromRtapiS64 PyLong_FromInt64 -#define PyLong_FromRtapiU32 PyLong_FromUInt32 -#define PyLong_FromRtapiU64 PyLong_FromUInt64 -#else -#define PyLong_FromRtapiS32 PyLong_FromLong -#define PyLong_FromRtapiS64 PyLong_FromLongLong -#define PyLong_FromRtapiU32 PyLong_FromUnsignedLong -#define PyLong_FromRtapiU64 PyLong_FromUnsignedLongLong -#endif +namespace py = pybind11; +namespace halxx = linuxcnc::hal; -// -// Thread-safe locale switcher for LC_NUMERIC to "C" that can be employed -// within a scoped block and automatically reverts to the previous locale when -// the class instance is destructed. -// -struct scoped_lc_numeric_c { - scoped_lc_numeric_c() { - // Make sure we always use the C locale for conversion (thread local) - // cppcheck-suppress useInitializationList - oldlc = uselocale(static_cast(NULL)); - newlc = newlocale(LC_NUMERIC_MASK, "C", static_cast(NULL)); - if(static_cast(NULL) == newlc) { - // FIXME: This is not nice, to print directly to stderr... - fprintf(stderr, "halmodule: internal error: Cannot set locale to \"C\" for numeric conversions"); - return; - } - uselocale(newlc); - } +int halquery_add_submodule(PyObject *); // halquery.cc - ~scoped_lc_numeric_c() { - if(static_cast(NULL) != newlc) { - uselocale(oldlc); - freelocale(newlc); - } - } -private: - locale_t oldlc; - locale_t newlc; -}; +// hal.error and the class objects the binding checks against. Created +// once at import and never released, as for the enums in halquery.cc, so +// nothing is freed during interpreter teardown. +static PyObject *pyhal_error_type = nullptr; +static PyTypeObject *comp_type = nullptr; +static py::handle rttype_enum; + +[[noreturn]] static void raise(PyObject *type, const std::string &msg) +{ + PyErr_SetString(type, msg.c_str()); + throw py::error_already_set(); +} -PyObject *to_python(bool b) { - return PyBool_FromLong(b); +// hal.error with the C library text for an errno, as for all library +// failures of a component. +[[noreturn]] static void raise_hal_errno(int code) +{ + raise(pyhal_error_type, strerror(code)); } -PyObject *to_python(rtapi_u32 u) { - return PyLong_FromRtapiU32(u); +[[noreturn]] static void raise_oserror(int code) +{ + errno = code; + PyErr_SetFromErrno(PyExc_OSError); + throw py::error_already_set(); } -PyObject *to_python(rtapi_s32 i) { - return PyLong_FromRtapiS32(i); +//---------------------------------------------------------------------- +// Python <=> HAL value conversion +//---------------------------------------------------------------------- + +// int(o), a new reference +static py::object as_pylong(PyObject *o) +{ + if(PyLong_Check(o)) + return py::reinterpret_borrow(o); + PyObject *t = PyNumber_Long(o); + if(!t) + throw py::error_already_set(); + return py::reinterpret_steal(t); } -PyObject *to_python(rtapi_uint u) { - return PyLong_FromRtapiU64(u); +static rtapi_sint to_sint(PyObject *o) +{ + py::object l = as_pylong(o); + long long v = PyLong_AsLongLong(l.ptr()); + if(-1 == v && PyErr_Occurred()) + throw py::error_already_set(); + return v; } -PyObject *to_python(rtapi_sint i) { - return PyLong_FromRtapiS64(i); +static rtapi_uint to_uint(PyObject *o) +{ + py::object l = as_pylong(o); + unsigned long long v = PyLong_AsUnsignedLongLong(l.ptr()); + if((unsigned long long)-1 == v && PyErr_Occurred()) + throw py::error_already_set(); + return v; } -PyObject *to_python(rtapi_real d) { - return PyFloat_FromDouble(d); +// The buffer size of a port signal. +static rtapi_uint to_u32(PyObject *o) +{ + rtapi_sint v = to_sint(o); + if(v < 0 || v > RTAPI_UINT32_MAX) + raise(PyExc_OverflowError, fmt::format("Value {} out of range", v)); + return (rtapi_uint)v; } -bool from_python(PyObject *o, bool *b) +static rtapi_bool to_bool(PyObject *o) { - // If it is a bool (True, False), we're fine - if(PyBool_Check(o)) { - *b = PyObject_IsTrue(o); - return true; - } - // Maybe a textual description of a bool + if(PyBool_Check(o)) + return Py_True == o; + // Text as in an ini file. Note that bool("False") is True in Python. if(PyUnicode_Check(o)) { - // Note that in python: bool("False") == True - // That means we cannot use the standard conversion if it is expected - // to work more natural and aligned with the ini-file format. - const char *cptr = PyUnicode_AsUTF8AndSize(o, NULL); - if (!cptr) { - PyErr_Format(PyExc_RuntimeError, "Invalid UTF-8 detected"); - return false; + const char *s = PyUnicode_AsUTF8AndSize(o, nullptr); + if(!s) { + PyErr_Clear(); + raise(PyExc_RuntimeError, "Invalid UTF-8 detected"); } static const struct { const char *name; bool value; } boolnames[] = { - // Both "1" and "0" could be caught with PyNumber_Long, but we're - // already here and it is easier this way. These are in the order - // in which you are most likely to see the value written. { "1", true }, { "0", false }, { "true", true }, @@ -135,416 +138,219 @@ bool from_python(PyObject *o, bool *b) { "off", false }, { "yes", true }, { "no", false }, - { NULL, false } // Termination }; - for(int i = 0; boolnames[i].name; i++) { - if (!strcasecmp(cptr, boolnames[i].name)) { - *b = boolnames[i].value; - return true; - } + for(const auto &bn : boolnames) { + if(!strcasecmp(s, bn.name)) + return bn.value; } } - - if(PyFloat_Check(o)) { - // Floating point is false *only* when it is 0.0 - double v = PyFloat_AsDouble(o); - *b = 0.0 != v; - return true; - } - - // Try the usual int(obj) conversion - PyObject *tmp = NULL; - long long l; - tmp = PyLong_Check(o) ? o : PyNumber_Long(o); - if(!tmp) goto fail; - - l = PyLong_AsLongLong(tmp); - if(-1 == l && PyErr_Occurred()) - goto fail; - - *b = l != 0; - if(tmp && tmp != o) Py_XDECREF(tmp); - return true; -fail: - if(tmp && tmp != o) Py_XDECREF(tmp); - return false; + // A float is false only when it is 0.0 + if(PyFloat_Check(o)) + return 0.0 != PyFloat_AsDouble(o); + return 0 != to_sint(o); } -bool from_python(PyObject *o, rtapi_real *d) { - if(PyFloat_Check(o)) { - *d = PyFloat_AsDouble(o); - return true; - } else if(PyLong_Check(o)) { - *d = PyLong_AsDouble(o); - return !PyErr_Occurred(); +static rtapi_real to_real(PyObject *o) +{ + if(PyFloat_Check(o)) + return PyFloat_AsDouble(o); + if(PyLong_Check(o)) { + double d = PyLong_AsDouble(o); + if(-1.0 == d && PyErr_Occurred()) + throw py::error_already_set(); + return d; + } + PyObject *t = PyNumber_Float(o); + if(!t) { + PyErr_Clear(); + raise(PyExc_TypeError, fmt::format("Number expected, not {}", Py_TYPE(o)->tp_name)); } + double d = PyFloat_AsDouble(t); + Py_DECREF(t); + return d; +} - // Ensure that float conversions are using decimal '.' - scoped_lc_numeric_c force_lc_numeric_c; - - PyObject *tmp = PyNumber_Float(o); - if(!tmp) { - PyErr_Format(PyExc_TypeError, "Number expected, not %s", Py_TYPE(o)->tp_name); - return false; +// A Python value as an exact HAL value of type t. A port takes its +// buffer size, which only a port signal can be set to. +static halxx::value_t value_from_python(hal_type_t t, PyObject *o) +{ + switch(t) { + case HAL_BOOL: return halxx::value_t(std::in_place_type, to_bool(o)); + case HAL_SINT: return halxx::value_t(std::in_place_type, to_sint(o)); + case HAL_UINT: return halxx::value_t(std::in_place_type, to_uint(o)); + case HAL_REAL: return halxx::value_t(std::in_place_type, to_real(o)); + case HAL_PORT: return halxx::value_t(std::in_place_type, to_u32(o)); + default: + raise(pyhal_error_type, fmt::format("Invalid item type {}", (int)t)); } - - *d = PyFloat_AsDouble(tmp); - Py_XDECREF(tmp); - return true; } -bool from_python(PyObject *o, rtapi_u32 *u) { - PyObject *tmp = NULL; - long long l; - tmp = PyLong_Check(o) ? o : PyNumber_Long(o); - if(!tmp) goto fail; - - l = PyLong_AsLongLong(tmp); - if(-1 == l && PyErr_Occurred()) goto fail; - if(l < 0 || l > RTAPI_UINT32_MAX) { - PyErr_Format(PyExc_OverflowError, "Value %lld out of range", l); - goto fail; - } +static py::object value_to_python(const halxx::value_t &v) +{ + return std::visit([](auto x) -> py::object { + using X = decltype(x); + if constexpr(std::is_same_v) + return py::bool_(x); + else if constexpr(std::is_same_v) + return py::float_(x); + else + return py::int_(x); + }, v); +} - *u = l; - if(tmp && tmp != o) Py_XDECREF(tmp); - return true; -fail: - if(tmp && tmp != o) Py_XDECREF(tmp); - return false; +static unsigned port_size(hal_port_t p) +{ + return 0 != hal_get_port(p) ? hal_port_buffer_size(p) : 0; } -bool from_python(PyObject *o, rtapi_s32 *i) { - PyObject *tmp = NULL; - long long l; - tmp = PyLong_Check(o) ? o : PyNumber_Long(o); - if(!tmp) goto fail; - - l = PyLong_AsLongLong(tmp); - if(-1 == l && PyErr_Occurred()) goto fail; - if(l < RTAPI_INT32_MIN || l > RTAPI_INT32_MAX) { - PyErr_Format(PyExc_OverflowError, "Value %lld out of range", l); - goto fail; +// The value behind a by-name query reference. A port reads as its buffer +// size when 'portsize' is set (pins and signals), else as 0. +static py::object ref_value(hal_type_t type, const hal_refs_u &ref, bool portsize) +{ + switch(type) { + case HAL_BOOL: return py::bool_(hal_get_bool(ref.b)); + case HAL_UINT: return py::int_(hal_get_uint(ref.u)); + case HAL_SINT: return py::int_(hal_get_sint(ref.s)); + case HAL_REAL: return py::float_(hal_get_real(ref.r)); + case HAL_PORT: return py::int_(portsize ? port_size(ref.p) : 0u); + default: return py::none(); } - - *i = l; - if(tmp && tmp != o) Py_XDECREF(tmp); - return true; -fail: - if(tmp && tmp != o) Py_XDECREF(tmp); - return false; } -bool from_python(PyObject *o, rtapi_uint *u) { - PyObject *tmp = NULL; - unsigned long long l; - tmp = PyLong_Check(o) ? o : PyNumber_Long(o); - if(!tmp) goto fail; - - l = PyLong_AsUnsignedLongLong(tmp); - if((unsigned long long)-1 == l && PyErr_Occurred()) - goto fail; - - *u = l; - if(tmp && tmp != o) Py_XDECREF(tmp); - return true; -fail: - if(tmp && tmp != o) Py_XDECREF(tmp); - return false; -} +//---------------------------------------------------------------------- +// hal.component and hal.item +//---------------------------------------------------------------------- +// The binding's classes have internal linkage, like the pybind11 types +// they hold. +namespace { -bool from_python(PyObject *o, rtapi_sint *i) { - PyObject *tmp = NULL; - long long l; - tmp = PyLong_Check(o) ? o : PyNumber_Long(o); - if(!tmp) goto fail; - - l = PyLong_AsLongLong(tmp); - if(-1 == l && PyErr_Occurred()) - goto fail; - - *i = l; - if(tmp && tmp != o) Py_XDECREF(tmp); - return true; -fail: - if(tmp && tmp != o) Py_XDECREF(tmp); - return false; -} +struct PyComp { + PyComp(const std::string &n, const std::string &prefix) + : name(n), c(std::make_unique(n)) { + c->setprefix(prefix); + } + bool live() const { return c && c->id() > 0; } -struct halitem { - bool is_pin; - hal_type_t type; - hal_pdir_t dir; - hal_refs_u *u; + std::string name; + std::unique_ptr c; // reset by exit() }; -struct pyhalitem { - PyObject_HEAD - halitem pin; - char * name; +// An item holds its component, so the HAL pin or param it refers to stays +// in place as long as the item exists. +struct PyItem { + py::object comp; + std::string name; // short name, without the prefix + halxx::anypin pin; }; -static PyObject * pyhal_pin_new(halitem * pin, const char *name); - -typedef std::map itemmap; - -typedef struct halobject { - PyObject_HEAD - int hal_id; - char *name; - char *prefix; - itemmap *items; -} halobject; +} // namespace -PyObject *pyhal_error_type = NULL; - -static PyObject *pyrtapi_error(int code) { - PyErr_SetString(pyhal_error_type, strerror(-code)); - return NULL; +// The component of 'self', which must not have exited. +static PyComp &live_comp(py::handle self) +{ + if(!PyObject_TypeCheck(self.ptr(), comp_type)) + raise(PyExc_TypeError, fmt::format("descriptor requires a 'hal.component' object but received a '{}'", + Py_TYPE(self.ptr())->tp_name)); + PyComp *pc = nullptr; + try { + pc = py::cast(self); + } catch(const py::cast_error &) { + pc = nullptr; + } + if(!pc || !pc->live()) + raise(PyExc_RuntimeError, "Invalid operation on closed HAL component"); + return *pc; } -static PyObject *pyhal_error(int code) { - PyErr_SetString(pyhal_error_type, strerror(-code)); - return NULL; +static halxx::anypin &find_item(PyComp &pc, const char *name) +{ + if(!pc.c->contains(name)) + raise(PyExc_AttributeError, fmt::format("Pin or param '{}' does not exist", name)); + return pc.c->item(name); } -static int pyhal_init(PyObject *_self, PyObject *args, PyObject *kw) { - (void)kw; - const char *name; - const char *prefix = NULL; - halobject *self = reinterpret_cast(_self); - - if(!PyArg_ParseTuple(args, "s|s:hal.component", &name, &prefix)) return -1; - - self->items = new itemmap(); - - self->hal_id = hal_init(name); - if(self->hal_id <= 0) { - pyhal_error(self->hal_id); - return -1; - } - - self->name = strdup(name); - self->prefix = strdup(prefix ? prefix : name); - if(!self->name) { - PyErr_SetString(PyExc_MemoryError, "strdup(name) failed"); - return -1; - } - if(!self->prefix) { - PyErr_SetString(PyExc_MemoryError, "strdup(prefix) failed"); - return -1; - } - - return 0; +// The value of an item: the buffer size for a port pin. +static py::object item_read(const halxx::anypin &p) +{ + if(p.is_port()) + return py::int_(p.is_pin() ? p.as_port().size() : 0u); + return value_to_python(p.get()); } -static void pyhal_exit_impl(halobject *self) { - if(self->hal_id > 0) - hal_exit(self->hal_id); - self->hal_id = 0; - - free(self->name); - self->name = NULL; - - free(self->prefix); - self->prefix = NULL; - - delete self->items; - self->items = NULL; +static void item_write(halxx::anypin &p, PyObject *v) +{ + hal_type_t t = p.type(); + if(HAL_PORT == t) + raise(pyhal_error_type, fmt::format("Invalid pin type {}", (int)t)); + p.set(value_from_python(t, v)); } -static void pyhal_delete(PyObject *_self) { - halobject *self = reinterpret_cast(_self); - pyhal_exit_impl(self); - Py_TYPE(self)->tp_free(self); +static const char *item_name(PyObject *name) +{ + const char *s = PyUnicode_AsUTF8(name); + if(!s) + throw py::error_already_set(); + return s; } -static int pyhal_write_common(halitem *pin, PyObject *value) { - if(!pin) return -1; - - switch(pin->type) { - case HAL_BOOL: { - bool tmp; - if(!from_python(value, &tmp)) return -1; - hal_set_bool(pin->u->b, tmp); - break; - } - case HAL_REAL: { - rtapi_real tmp; - if(!from_python(value, &tmp)) return -1; - hal_set_real(pin->u->r, tmp); - break; - } - case HAL_UINT: { - rtapi_uint tmp; - if(!from_python(value, &tmp)) return -1; - hal_set_uint(pin->u->u, tmp); - break; - } - case HAL_SINT: { - rtapi_sint tmp; - if(!from_python(value, &tmp)) return -1; - hal_set_sint(pin->u->s, tmp); - break; - } - default: - PyErr_Format(pyhal_error_type, "Invalid pin type %d", pin->type); - } - return 0; +// Attribute and item lookup: the normal lookup first (methods, class and +// instance attributes), then the value of the pin or param of that name. +static py::object comp_getattr(py::handle self, py::handle name) +{ + PyComp &pc = live_comp(self); + PyObject *r = PyObject_GenericGetAttr(self.ptr(), name.ptr()); + if(r) + return py::reinterpret_steal(r); + PyErr_Clear(); + return item_read(find_item(pc, item_name(name.ptr()))); } -static PyObject *pyhal_read_common(halitem *item) { - if(!item) return NULL; - switch(item->type) { - case HAL_BOOL: return to_python(hal_get_bool(item->u->b)); - case HAL_UINT: return to_python(hal_get_uint(item->u->u)); - case HAL_SINT: return to_python(hal_get_sint(item->u->s)); - case HAL_REAL: return to_python(hal_get_real(item->u->r)); - case HAL_PORT: - if(item->is_pin) - return to_python(hal_port_buffer_size(item->u->p)); - else - return to_python((unsigned)0); // HAL_PORT cannot be a parameter - default: - break; - } - PyErr_Format(pyhal_error_type, "Invalid item type %d", item->type); - return NULL; +// Attribute and item assignment always write a pin or param. +static void comp_setattr(py::handle self, py::handle name, py::handle v) +{ + PyComp &pc = live_comp(self); + item_write(find_item(pc, item_name(name.ptr())), v.ptr()); } -static halitem *find_item(halobject *self, const char *name) { - if(!name) return NULL; - - itemmap::iterator i = self->items->find(name); - - if(i == self->items->end()) { - PyErr_Format(PyExc_AttributeError, "Pin or param '%s' does not exist", name); - return NULL; - } - - return &(i->second); +static void comp_delattr(py::handle self, py::handle name) +{ + PyComp &pc = live_comp(self); + const char *s = item_name(name.ptr()); + find_item(pc, s); + raise(PyExc_TypeError, fmt::format("Pin or param '{}' cannot be deleted", s)); } -static bool is_valid_hal_type(hal_type_t t, bool allowport) +static py::object comp_new_item(py::handle self, const std::string &name, int type, int dir, bool ispin) { - switch(t) { + PyComp &pc = live_comp(self); + if(pc.c->contains(name)) + raise(PyExc_ValueError, fmt::format("Duplicate {} name '{}'", ispin ? "pin" : "parameter", name)); + switch(type) { case HAL_BOOL: case HAL_SINT: case HAL_UINT: case HAL_REAL: - return true; + break; case HAL_PORT: - return allowport; + if(ispin) + break; + [[fallthrough]]; default: - return false; + raise(pyhal_error_type, fmt::format("Invalid {} type {}", ispin ? "pin" : "param", type)); } -} - -static PyObject * pyhal_create_param(halobject *self, const char *name, hal_type_t type, hal_pdir_t dir) { - int res; - halitem param; - param.is_pin = 0; - - if(!is_valid_hal_type(type, false)) { - PyErr_Format(pyhal_error_type, "Invalid param type %d", type); - return NULL; - } - - param.type = type; - param.dir = dir; - param.u = (hal_refs_u *)hal_malloc(sizeof(*param.u)); - if(!param.u) { - PyErr_SetString(PyExc_MemoryError, "hal_malloc failed"); - return NULL; - } - - switch(type) { - case HAL_BOOL: res = hal_param_new_bool(self->hal_id, dir, ¶m.u->b, 0, "%s.%s", self->prefix, name); break; - case HAL_SINT: res = hal_param_new_sint(self->hal_id, dir, ¶m.u->s, 0, "%s.%s", self->prefix, name); break; - case HAL_UINT: res = hal_param_new_uint(self->hal_id, dir, ¶m.u->u, 0, "%s.%s", self->prefix, name); break; - case HAL_REAL: res = hal_param_new_real(self->hal_id, dir, ¶m.u->r, 0.0, "%s.%s", self->prefix, name); break; - default: res = -EINVAL; break; + const std::string &prefix = pc.c->getprefix(); + if(ispin && prefix.size() + 1 + name.size() > HAL_NAME_LEN) + raise(pyhal_error_type, fmt::format("Invalid pin name length \"{}.{}\": max = {} characters", + prefix, name, HAL_NAME_LEN)); + try { + halxx::anypin p = ispin ? pc.c->newpin(name, (hal_type_t)type, (halxx::dir)dir) + : pc.c->newparam(name, (hal_type_t)type, (halxx::dir)dir); + return py::cast(PyItem{py::reinterpret_borrow(self), name, std::move(p)}); + } catch(const std::system_error &e) { + raise_hal_errno(e.code().value()); + } catch(const std::bad_alloc &) { + raise(PyExc_MemoryError, "hal_malloc failed"); } - if(res) return pyhal_error(res); - - (*self->items)[name] = param; - - return pyhal_pin_new(¶m, name); -} - - -static PyObject * pyhal_create_pin(halobject *self, const char *name, hal_type_t type, hal_pdir_t dir) { - char pin_name[HAL_NAME_LEN+1]; - int res; - halitem pin; - pin.is_pin = 1; - - if(!is_valid_hal_type(type, true)) { - PyErr_Format(pyhal_error_type, "Invalid pin type %d", type); - return NULL; - } - - pin.type = type; - pin.dir = dir; - pin.u = (hal_refs_u *)hal_malloc(sizeof(*pin.u)); - if(!pin.u) { - PyErr_SetString(PyExc_MemoryError, "hal_malloc failed"); - return NULL; - } - - res = snprintf(pin_name, sizeof(pin_name), "%s.%s", self->prefix, name); - if(res > HAL_NAME_LEN || res < 0) { - PyErr_Format(pyhal_error_type, - "Invalid pin name length \"%s.%s\": max = %d characters", - self->prefix, name, HAL_NAME_LEN); - return NULL; - } - switch(type) { - case HAL_BOOL: res = hal_pin_new_bool(self->hal_id, dir, &pin.u->b, 0, "%s.%s", self->prefix, name); break; - case HAL_SINT: res = hal_pin_new_sint(self->hal_id, dir, &pin.u->s, 0, "%s.%s", self->prefix, name); break; - case HAL_UINT: res = hal_pin_new_uint(self->hal_id, dir, &pin.u->u, 0, "%s.%s", self->prefix, name); break; - case HAL_REAL: res = hal_pin_new_real(self->hal_id, dir, &pin.u->r, 0.0, "%s.%s", self->prefix, name); break; - case HAL_PORT: res = hal_pin_new_port(self->hal_id, dir, &pin.u->p, "%s.%s", self->prefix, name); break; - default: res = -EINVAL; break; - } - if(res) return pyhal_error(res); - - (*self->items)[name] = pin; - - return pyhal_pin_new(&pin, name); -} - -static PyObject *pyhal_new_param(PyObject *_self, PyObject *o) { - const char *name; - int type, dir; - halobject *self = reinterpret_cast(_self); - - if(!PyArg_ParseTuple(o, "sii", &name, &type, &dir)) - return NULL; - EXCEPTION_IF_NOT_LIVE(NULL); - - if (find_item(self, name)) { - PyErr_Format(PyExc_ValueError, "Duplicate parameter name '%s'", name); - return NULL; - } else { PyErr_Clear(); } - return pyhal_create_param(self, name, (hal_type_t)type, (hal_pdir_t)dir); -} - - -static PyObject *pyhal_new_pin(PyObject *_self, PyObject *o) { - const char *name; - int type, dir; - halobject *self = reinterpret_cast(_self); - - if(!PyArg_ParseTuple(o, "sii", &name, &type, &dir)) - return NULL; - EXCEPTION_IF_NOT_LIVE(NULL); - - if (find_item(self, name)) { - PyErr_Format(PyExc_ValueError, "Duplicate pin name '%s'", name); - return NULL; - } else { PyErr_Clear(); } - return pyhal_create_pin(self, name, (hal_type_t)type, (hal_pdir_t)dir); } enum what_type_e { @@ -552,254 +358,23 @@ enum what_type_e { WHAT_PIN, WHAT_PARAM, }; -static PyObject *get_pin_or_param(halobject *self, PyObject *args, what_type_e what) +static py::object comp_get_item(py::handle self, const std::string &name, what_type_e what) { - const char *name; - if(!PyArg_ParseTuple(args, "s", &name)) - return NULL; - EXCEPTION_IF_NOT_LIVE(NULL); - - halitem *pin = find_item(self, name); - if(!pin) - return NULL; - if(what == WHAT_ANY || (pin->is_pin && what == WHAT_PIN) || (!pin->is_pin && what == WHAT_PARAM)) - return pyhal_pin_new(pin, name); - PyErr_Format(PyExc_AttributeError, "%s '%s' does not exist", what == WHAT_PIN ? "Pin" : "Param", name); - return NULL; -} - -static PyObject *pyhal_get_pin(PyObject *self, PyObject *args) -{ - return get_pin_or_param(reinterpret_cast(self), args, WHAT_PIN); + PyComp &pc = live_comp(self); + halxx::anypin &p = find_item(pc, name.c_str()); + if(what == WHAT_ANY || (p.is_pin() && what == WHAT_PIN) || (!p.is_pin() && what == WHAT_PARAM)) + return py::cast(PyItem{py::reinterpret_borrow(self), name, p.dup()}); + raise(PyExc_AttributeError, fmt::format("{} '{}' does not exist", what == WHAT_PIN ? "Pin" : "Param", name)); } -static PyObject *pyhal_get_param(PyObject *self, PyObject *args) +static PyComp &live_item(const PyItem &it) { - return get_pin_or_param(reinterpret_cast(self), args, WHAT_PARAM); + return live_comp(it.comp); } -static PyObject *pyhal_get_item(PyObject *self, PyObject *args) +static const char *pin_type2name(hal_type_t type) { - return get_pin_or_param(reinterpret_cast(self), args, WHAT_ANY); -} - -static PyObject *pyhal_get_pins(PyObject *_self, PyObject * /*o*/) { - halobject *self = reinterpret_cast(_self); - - EXCEPTION_IF_NOT_LIVE(NULL); - - PyObject *d = PyDict_New(); - for(itemmap::iterator i = self->items->begin(); i != self->items->end(); ++i) { - halitem * pin = &(i->second); - PyObject *key = PyUnicode_FromString(i->first.c_str()); - if(!key) { - Py_DECREF(d); - return NULL; - } - PyObject *val = pyhal_read_common(pin); - if(!val) { - Py_DECREF(key); - Py_DECREF(d); - return NULL; - } - PyDict_SetItem(d, key, val); - Py_DECREF(val); - Py_DECREF(key); - } - return d; -} - - -static PyObject *pyhal_ready(PyObject *_self, PyObject * /*o*/) { - // hal_ready did not exist in EMC 2.0.x, make it a no-op - halobject *self = reinterpret_cast(_self); - EXCEPTION_IF_NOT_LIVE(NULL); - int res = hal_ready(self->hal_id); - if(res) return pyhal_error(res); - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject *pyhal_unready(PyObject *_self, PyObject * /*o*/) { - // hal_ready did not exist in EMC 2.0.x, make it a no-op - halobject *self = reinterpret_cast(_self); - EXCEPTION_IF_NOT_LIVE(NULL); - int res = hal_unready(self->hal_id); - if(res) return pyhal_error(res); - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject *pyhal_exit(PyObject *_self, PyObject * /*o*/) { - halobject *self = reinterpret_cast(_self); - pyhal_exit_impl(self); - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject *pyhal_repr(PyObject *_self) { - halobject *self = reinterpret_cast(_self); - return PyUnicode_FromFormat("", - self->name, self->hal_id, (int)self->items->size()); -} - -static PyObject *pyhal_getattro(PyObject *_self, PyObject *attro) { - PyObject *result; - halobject *self = reinterpret_cast(_self); - EXCEPTION_IF_NOT_LIVE(NULL); - - result = PyObject_GenericGetAttr(reinterpret_cast(self), attro); - if(result) return result; - - PyErr_Clear(); - return pyhal_read_common(find_item(self, PyUnicode_AsUTF8(attro))); -} - -static int pyhal_setattro(PyObject *_self, PyObject *attro, PyObject *v) { - halobject *self = reinterpret_cast(_self); - // FIXME: The documentation states that when v==NULL it means to delete the - // attribute and it must be supported. - EXCEPTION_IF_NOT_LIVE(-1); - return pyhal_write_common(find_item(self, PyUnicode_AsUTF8(attro)), v); -} - -static Py_ssize_t pyhal_len(PyObject *_self) { - halobject* self = reinterpret_cast(_self); - EXCEPTION_IF_NOT_LIVE(-1); - return self->items->size(); -} - -static PyObject *pyhal_get_prefix(PyObject *_self, PyObject *args) { - halobject* self = reinterpret_cast(_self); - if(!PyArg_ParseTuple(args, "")) return NULL; - EXCEPTION_IF_NOT_LIVE(NULL); - - if(!self->prefix) { - Py_INCREF(Py_None); - return Py_None; - } - - return PyUnicode_FromString(self->prefix); -} - - -static PyObject *pyhal_set_prefix(PyObject *_self, PyObject *args) { - const char *newprefix; - halobject* self = reinterpret_cast(_self); - if(!PyArg_ParseTuple(args, "s", &newprefix)) return NULL; - EXCEPTION_IF_NOT_LIVE(NULL); - - if(self->prefix) - free(self->prefix); - self->prefix = strdup(newprefix); - - if(!self->prefix) { - PyErr_SetString(PyExc_MemoryError, "strdup(prefix) failed"); - return NULL; - } - - Py_INCREF(Py_None); - return Py_None; -} - -static PyMethodDef hal_methods[] = { - {"setprefix", pyhal_set_prefix, METH_VARARGS, - "Set the prefix for newly created pins and parameters"}, - {"getprefix", pyhal_get_prefix, METH_VARARGS, - "Get the prefix for newly created pins and parameters"}, - {"newparam", pyhal_new_param, METH_VARARGS, - "Create a new parameter"}, - {"newpin", pyhal_new_pin, METH_VARARGS, - "Create a new pin"}, - {"getitem", pyhal_get_item, METH_VARARGS, - "Get existing pin or param object"}, - {"getpin", pyhal_get_pin, METH_VARARGS, - "Get existing pin object"}, - {"getparam", pyhal_get_param, METH_VARARGS, - "Get existing param object"}, - {"getpins", pyhal_get_pins, METH_VARARGS, - "Get all pins and values of component"}, - {"exit", pyhal_exit, METH_NOARGS, - "Call hal_exit"}, - {"ready", pyhal_ready, METH_NOARGS, - "Call hal_ready"}, - {"unready", pyhal_unready, METH_NOARGS, - "Call hal_unready"}, - {}, -}; - -static PyMappingMethods halobject_map = { - pyhal_len, - pyhal_getattro, - pyhal_setattro -}; - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant" -static -PyTypeObject halobject_type = { - PyVarObject_HEAD_INIT(NULL, 0) - "hal.component", /*tp_name*/ - sizeof(halobject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - pyhal_delete, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - pyhal_repr, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - &halobject_map, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - 0, /*tp_str*/ - pyhal_getattro, /*tp_getattro*/ - pyhal_setattro, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /*tp_flags*/ - "HAL Component", /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - hal_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ - 0, /*tp_base*/ - 0, /*tp_dict*/ - 0, /*tp_descr_get*/ - 0, /*tp_descr_set*/ - 0, /*tp_dictoffset*/ - pyhal_init, /*tp_init*/ - 0, /*tp_alloc*/ - PyType_GenericNew, /*tp_new*/ - 0, /*tp_free*/ - 0, /*tp_is_gc*/ - 0, /*tp_bases*/ - 0, /*tp_mro*/ - 0, /*tp_cache*/ - 0, /*tp_subclasses*/ - 0, /*tp_weaklink*/ - 0, /*tp_del*/ - 0, /*tp_version_tag*/ - 0, /*tp_finalize*/ -#if PY_VERSION_HEX >= 0x030800f0 // 3.8 - 0, /*tp_vectorcall*/ -#if PY_VERSION_HEX >= 0x030c00f0 // 3.12 - 0, /*tp_watched*/ -#if PY_VERSION_HEX >= 0x030d00f0 // 3.13 - 0, /*tp_versions_used*/ -#endif -#endif -#endif -}; -#pragma GCC diagnostic pop - -static const char * pin_type2name(hal_type_t type) { - switch (type) { + switch(type) { case HAL_BOOL: return "BOOL"; case HAL_SINT: return "SINT"; case HAL_UINT: return "UINT"; @@ -809,8 +384,9 @@ static const char * pin_type2name(hal_type_t type) { } } -static const char * pin_dir2name(hal_pdir_t type) { - switch (type) { +static const char *pin_dir2name(halxx::dir d) +{ + switch((hal_pdir_t)d) { case HAL_IN: return "IN"; case HAL_IO: return "IO"; case HAL_OUT: return "OUT"; @@ -820,655 +396,393 @@ static const char * pin_dir2name(hal_pdir_t type) { } } -static PyObject *pyhalpin_repr(PyObject *_self) { - pyhalitem *pyself = reinterpret_cast(_self); - halitem *self = &pyself->pin; - - const char * name = "(null)"; - if (pyself->name) name = pyself->name; - - const char *pp = self->is_pin ? "pin" : "param"; - return PyUnicode_FromFormat("", pp, name, - pin_type2name(self->type), pin_dir2name(self->dir)); -} - -static int pyhalpin_init(PyObject * /*_self*/, PyObject *, PyObject *) { - PyErr_Format(PyExc_RuntimeError, "Cannot be constructed directly"); - return -1; -} - -static void pyhalpin_delete(PyObject *_self) { - pyhalitem *self = reinterpret_cast(_self); - - if(self->name) free(self->name); - - PyObject_Del(self); -} - -static PyObject * pyhal_pin_set(PyObject * _self, PyObject * value) { - pyhalitem * self = reinterpret_cast(_self); - if (pyhal_write_common(&self->pin, value) == -1) - return NULL; - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * pyhal_pin_get(PyObject * _self, PyObject *) { - pyhalitem * self = reinterpret_cast(_self); - return pyhal_read_common(&self->pin); -} - -static PyObject * pyhal_pin_get_type(PyObject * _self, PyObject *) { - pyhalitem * self = reinterpret_cast(_self); - return PyLong_FromLong(self->pin.type); -} - -static PyObject * pyhal_pin_get_dir(PyObject * _self, PyObject *) { - pyhalitem * self = reinterpret_cast(_self); - return PyLong_FromLong(self->pin.dir); -} - -static PyObject * pyhal_pin_is_pin(PyObject * _self, PyObject *) { - pyhalitem * self = reinterpret_cast(_self); - return PyBool_FromLong(self->pin.is_pin); +//---------------------------------------------------------------------- +// Port pin methods. Reads and writes are all or nothing; a count out of +// range or more than the port holds returns False. +//---------------------------------------------------------------------- +static const halxx::port &check_port(const PyItem &it, const char *pfx) +{ + live_item(it); + if(!it.pin.is_pin()) + raise(PyExc_RuntimeError, fmt::format("{}: {}: Not a pin", pfx, it.name)); + if(!it.pin.is_port()) + raise(PyExc_RuntimeError, fmt::format("{}: {}: Pin type not HAL_PORT but '{}'", pfx, it.name, (int)it.pin.type())); + return it.pin.as_port(); } -static PyObject * pyhal_pin_get_name(PyObject * _self, PyObject *) { - pyhalitem * self = reinterpret_cast(_self); - if (!self->name) { - Py_INCREF(Py_None); - return Py_None; - } - return PyUnicode_FromString(self->name); +static void check_dir(const PyItem &it, const char *pfx, halxx::dir d) +{ + if(it.pin.direction() != d) + raise(PyExc_RuntimeError, fmt::format("{}: {}: Pin not {}", pfx, it.name, + halxx::dir::OUT == d ? "output" : "input")); } -// - - - - -// Port methods -static bool check_port(const pyhalitem *item, const char *pfx) +static unsigned long port_count(const PyItem &it, const char *pfx, py::handle o) { - if(!item->pin.is_pin) { - PyErr_Format(PyExc_RuntimeError, "%s: %s: Not a pin", pfx, item->name); - return false; - } - if(item->pin.type != HAL_PORT) { - PyErr_Format(PyExc_RuntimeError, "%s: %s: Pin type not HAL_PORT but '%d'", pfx, item->name, (int)item->pin.type); - return false; - } - return true; + if(!PyLong_Check(o.ptr())) + raise(PyExc_RuntimeError, fmt::format("{}: {}: Argument not an integer", pfx, it.name)); + unsigned long l = PyLong_AsUnsignedLong(o.ptr()); + if((unsigned long)-1 == l && PyErr_Occurred()) + throw py::error_already_set(); + return l; } -static PyObject *pyhal_port_write(PyObject *self, PyObject *o) +static py::object port_write(const PyItem &it, py::handle o) { - pyhalitem *item = reinterpret_cast(self); - if(!check_port(item, "write")) - return NULL; - if(item->pin.dir != HAL_OUT) { - PyErr_Format(PyExc_RuntimeError, "write: %s: Pin not output", item->name); - return NULL; - } + const halxx::port &p = check_port(it, "write"); + check_dir(it, "write", halxx::dir::OUT); - Py_ssize_t len; + Py_ssize_t len = 0; const char *cptr; - if(PyUnicode_Check(o)) { - cptr = PyUnicode_AsUTF8AndSize(o, &len); - } else if(PyBytes_Check(o)) { - cptr = PyBytes_AsString(o); - len = PyBytes_Size(o); + if(PyUnicode_Check(o.ptr())) { + cptr = PyUnicode_AsUTF8AndSize(o.ptr(), &len); + } else if(PyBytes_Check(o.ptr())) { + cptr = PyBytes_AsString(o.ptr()); + len = PyBytes_Size(o.ptr()); } else { - PyErr_Format(PyExc_RuntimeError, "write: %s: Argument not a string or bytes object", item->name); - return NULL; + raise(PyExc_RuntimeError, fmt::format("write: {}: Argument not a string or bytes object", it.name)); } - if(!cptr) { - PyErr_Format(PyExc_RuntimeError, "write: %s: Object conversion to bytes failed", item->name); - return NULL; - } - if(len < 1 || len > HAL_PORT_SIZE_MAX) { - Py_INCREF(Py_False); - return Py_False; - } - if(len > (Py_ssize_t)hal_port_writable(item->pin.u->p)) { - Py_INCREF(Py_False); - return Py_False; + PyErr_Clear(); + raise(PyExc_RuntimeError, fmt::format("write: {}: Object conversion to bytes failed", it.name)); } - - return PyBool_FromLong(hal_port_write(item->pin.u->p, cptr, (unsigned)len)); + if(len < 1 || len > HAL_PORT_SIZE_MAX || len > (Py_ssize_t)p.writable()) + return py::bool_(false); + return py::bool_(p.write(cptr, (unsigned)len)); } -static PyObject *pyhal_port_read_peek(pyhalitem *item, PyObject *o, bool isread) +static py::object port_read_peek(const PyItem &it, py::handle o, bool isread) { const char *pfx = isread ? "read" : "peek"; - if(!check_port(item, pfx)) - return NULL; - if(item->pin.dir != HAL_IN) { - PyErr_Format(PyExc_RuntimeError, "%s: %s: Pin not input", pfx, item->name); - return NULL; - } - if(!PyLong_Check(o)) { - PyErr_Format(PyExc_RuntimeError, "%s: %s: Argument not an integer", pfx, item->name); - return NULL; - } - unsigned long l = PyLong_AsUnsignedLong(o); - if((unsigned long)-1 == l && PyErr_Occurred()) { - return NULL; - } - if(l < 1 || l > HAL_PORT_SIZE_MAX) { - Py_INCREF(Py_False); - return Py_False; - } - if(l > (unsigned long)hal_port_readable(item->pin.u->p)) { - Py_INCREF(Py_False); - return Py_False; - } - - PyObject *bts = PyBytes_FromStringAndSize(NULL, (Py_ssize_t)l); - if(!bts) { - PyErr_Format(PyExc_RuntimeError, "%s: %s: Failed to create bytes object", pfx, item->name); - return NULL; - } - bool b; - if(isread) - b = hal_port_read(item->pin.u->p, PyBytes_AsString(bts), (unsigned)l); - else - b = hal_port_peek(item->pin.u->p, PyBytes_AsString(bts), (unsigned)l); - if(!b) { - Py_DECREF(bts); - Py_INCREF(Py_False); - return Py_False; - } - return bts; -} + const halxx::port &p = check_port(it, pfx); + check_dir(it, pfx, halxx::dir::IN); + unsigned long l = port_count(it, pfx, o); + if(l < 1 || l > HAL_PORT_SIZE_MAX || l > p.readable()) + return py::bool_(false); + std::string buf(l, '\0'); + if(!(isread ? p.read(buf.data(), (unsigned)l) : p.peek(buf.data(), (unsigned)l))) + return py::bool_(false); + return py::bytes(buf); +} + +//---------------------------------------------------------------------- +// hal.stream +//---------------------------------------------------------------------- +// The stream member is declared last, so it is closed before the +// reference to its component is dropped. +namespace { +struct PyStream { + PyStream(py::object c, halxx::component &hc, int key, unsigned depth, const std::string &ts) + : comp(std::move(c)), s(hc, key, depth, ts) {} + PyStream(py::object c, halxx::component &hc, int key, const std::string &ts) + : comp(std::move(c)), s(hc, key, ts) {} + + py::object comp; + halxx::stream s; +}; +} // namespace -static PyObject *pyhal_port_read(PyObject *self, PyObject *o) +// A positional 'int' argument of a constructor that takes *args. +static int int_arg(py::handle o, const char *fn, int argno) { - pyhalitem *item = reinterpret_cast(self); - return pyhal_port_read_peek(item, o, 1); -} - -static PyObject *pyhal_port_peek(PyObject *self, PyObject *o) + if(PyFloat_Check(o.ptr())) + raise(PyExc_TypeError, fmt::format("{}() argument {} must be int, not float", fn, argno)); + PyObject *idx = PyNumber_Index(o.ptr()); + if(!idx) + throw py::error_already_set(); + long v = PyLong_AsLong(idx); + Py_DECREF(idx); + if(-1 == v && PyErr_Occurred()) + throw py::error_already_set(); + if(v < INT_MIN || v > INT_MAX) + raise(PyExc_OverflowError, "signed integer is out of range"); + return (int)v; +} + +static std::string str_arg(py::handle o, const char *fn, int argno) { - pyhalitem *item = reinterpret_cast(self); - return pyhal_port_read_peek(item, o, 0); + if(!PyUnicode_Check(o.ptr())) + raise(PyExc_TypeError, fmt::format("{}() argument {} must be str, not {}", fn, argno, Py_TYPE(o.ptr())->tp_name)); + Py_ssize_t len; + const char *s = PyUnicode_AsUTF8AndSize(o.ptr(), &len); + if(!s) + throw py::error_already_set(); + return std::string(s, len); } -static PyObject *pyhal_port_peek_commit(PyObject *self, PyObject *o) +// hal.stream(comp, key, depth, typestring) creates a stream, as does +// any call with four arguments and a non-zero depth; hal.stream(comp, +// key[, typestring]) attaches to one. Keyword arguments are ignored. +static std::unique_ptr stream_new(py::args args, py::kwargs) { - pyhalitem *item = reinterpret_cast(self); - if(!check_port(item, "peek_commit")) - return NULL; - if(!PyLong_Check(o)) { - PyErr_Format(PyExc_RuntimeError, "peek_commit: %s: Argument not an integer", item->name); - return NULL; - } - unsigned long l = PyLong_AsUnsignedLong(o); - if((unsigned long)-1 == l && PyErr_Occurred()) { - return NULL; - } - if(l < 1 || l > HAL_PORT_SIZE_MAX) { - Py_INCREF(Py_False); - return Py_False; - } - return PyBool_FromLong(hal_port_peek_commit(item->pin.u->p, (unsigned)l)); -} - -static PyObject *pyhal_port_writable(PyObject *self, PyObject *) + size_t n = args.size(); + if(n < 2 || n > 4) + raise(PyExc_TypeError, fmt::format("hal.stream() takes from 2 to 4 positional arguments but {} were given", n)); + if(!PyObject_TypeCheck(args[0].ptr(), comp_type)) + raise(PyExc_TypeError, fmt::format("hal.stream() argument 1 must be hal.component, not {}", + Py_TYPE(args[0].ptr())->tp_name)); + int key = int_arg(args[1], "hal.stream", 2); + int depth = 0; + std::string typestring; + if(4 == n) { + depth = int_arg(args[2], "hal.stream", 3); + typestring = str_arg(args[3], "hal.stream", 4); + } else if(3 == n) { + typestring = str_arg(args[2], "hal.stream", 3); + } + PyComp &pc = live_comp(args[0]); + py::object comp = py::reinterpret_borrow(args[0]); + try { + if(depth) + return std::make_unique(comp, *pc.c, key, (unsigned)depth, typestring); + return std::make_unique(comp, *pc.c, key, typestring); + } catch(const std::system_error &e) { + raise_oserror(e.code().value()); + } +} + +static py::object stream_read(PyStream &ps) { - pyhalitem *item = reinterpret_cast(self); - if(!check_port(item, "writable")) - return NULL; - if(item->pin.dir != HAL_OUT) { - PyErr_Format(PyExc_RuntimeError, "writable: %s: Pin not output", item->name); - return NULL; - } - return PyLong_FromUnsignedLong(hal_port_writable(item->pin.u->p)); + auto sample = ps.s.read(); + if(!sample) + return py::none(); + py::tuple t(sample->size()); + for(size_t i = 0; i < sample->size(); i++) + t[i] = value_to_python((*sample)[i]); + return t; } -static PyObject *pyhal_port_readable(PyObject *self, PyObject *) +static void stream_write(PyStream &ps, py::handle data) { - pyhalitem *item = reinterpret_cast(self); - if(!check_port(item, "readable")) - return NULL; - if(item->pin.dir != HAL_IN) { - PyErr_Format(PyExc_RuntimeError, "readable: %s: Pin not input", item->name); - return NULL; - } - return PyLong_FromUnsignedLong(hal_port_readable(item->pin.u->p)); -} - -static PyObject *pyhal_port_clear(PyObject *self, PyObject *) + if(!PyTuple_Check(data.ptr())) + raise(PyExc_TypeError, fmt::format("hal.stream.write() argument 1 must be tuple, not {}", + Py_TYPE(data.ptr())->tp_name)); + size_t n = (size_t)ps.s.element_count(); + size_t nd = (size_t)PyTuple_GET_SIZE(data.ptr()); + if(nd > n) + raise(PyExc_ValueError, "Too many elements to unpack"); + if(nd < n) + raise(PyExc_ValueError, "Too few elements to unpack"); + std::vector buf; + buf.reserve(n); + for(size_t i = 0; i < n; i++) + buf.push_back(value_from_python(ps.s.element_type((int)i), PyTuple_GET_ITEM(data.ptr(), i))); + try { + ps.s.write(buf); + } catch(const std::system_error &e) { + raise_oserror(e.code().value()); + } +} + +//---------------------------------------------------------------------- +// Module functions +//---------------------------------------------------------------------- +[[noreturn]] static void raise_query(const char *fn, const std::string &name, const std::system_error &e) { - pyhalitem *item = reinterpret_cast(self); - if(!check_port(item, "clear")) - return NULL; - hal_port_clear(item->pin.u->p); - Py_INCREF(Py_None); - return Py_None; + raise(PyExc_RuntimeError, fmt::format("{}: {}: {}", fn, name, hal_strerror(-e.code().value()))); } -static PyObject *pyhal_port_size(PyObject *self, PyObject *) +static py::object pin_has_writer(const std::string &name) { - pyhalitem *item = reinterpret_cast(self); - if(!check_port(item, "size")) - return NULL; - return PyLong_FromUnsignedLong(hal_port_buffer_size(item->pin.u->p)); -} - -static PyMethodDef halpin_methods[] = { - {"set", pyhal_pin_set, METH_O, "Set item value"}, - {"get", pyhal_pin_get, METH_NOARGS, "Get item value"}, - {"get_type", pyhal_pin_get_type, METH_NOARGS, "Get item type"}, - {"get_dir", pyhal_pin_get_dir, METH_NOARGS, "Get item direction"}, - {"get_name", pyhal_pin_get_name, METH_NOARGS, "Get item name"}, - {"is_pin", pyhal_pin_is_pin, METH_NOARGS, "If item is pin or param"}, - // Port methods - {"write", pyhal_port_write, METH_O, "Write data to the port queue"}, - {"read", pyhal_port_read, METH_O, "Read data from the port queue"}, - {"writable", pyhal_port_writable, METH_NOARGS, "Get the count that can be written to the port"}, - {"readable", pyhal_port_readable, METH_NOARGS, "Get the count that can be read from the port"}, - {"peek", pyhal_port_peek, METH_O, "Read pending data without removal from the queue"}, - {"peek_commit", pyhal_port_peek_commit, METH_O, "Advance the read pointer by given amount"}, - {"clear", pyhal_port_clear, METH_NOARGS, "Clear the port queue"}, - {"size", pyhal_port_size, METH_NOARGS, "Get the queue size of the port"}, - {}, -}; - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant" -static -PyTypeObject halpin_type = { - PyVarObject_HEAD_INIT(NULL, 0) - "hal.item", /*tp_name*/ - sizeof(pyhalitem), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - pyhalpin_delete, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - pyhalpin_repr, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - "HAL Pin", /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - halpin_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ - 0, /*tp_base*/ - 0, /*tp_dict*/ - 0, /*tp_descr_get*/ - 0, /*tp_descr_set*/ - 0, /*tp_dictoffset*/ - pyhalpin_init, /*tp_init*/ - 0, /*tp_alloc*/ - PyType_GenericNew, /*tp_new*/ - 0, /*tp_free*/ - 0, /*tp_is_gc*/ - 0, /*tp_bases*/ - 0, /*tp_mro*/ - 0, /*tp_cache*/ - 0, /*tp_subclasses*/ - 0, /*tp_weaklink*/ - 0, /*tp_del*/ - 0, /*tp_version_tag*/ - 0, /*tp_finalize*/ -#if PY_VERSION_HEX >= 0x030800f0 // 3.8 - 0, /*tp_vectorcall*/ -#if PY_VERSION_HEX >= 0x030c00f0 // 3.12 - 0, /*tp_watched*/ -#if PY_VERSION_HEX >= 0x030d00f0 // 3.13 - 0, /*tp_versions_used*/ -#endif -#endif -#endif -}; -#pragma GCC diagnostic pop - -static PyObject * pyhal_pin_new(halitem * pin, const char * name) { - pyhalitem * pypin = PyObject_New(pyhalitem, &halpin_type); - if (!pypin) - return NULL; - pypin->pin = *pin; - if (name) - pypin->name = strdup(name); - else - pypin->name = NULL; - - return reinterpret_cast(pypin); -} - -PyObject *pin_has_writer(PyObject * /*self*/, PyObject *args) { - hal_query_t q = {}; - if(!PyArg_ParseTuple(args, "s", &q.name)) return NULL; - - q.qtype = HAL_QTYPE_PIN; // Only query a pin - int rv = hal_get_p(&q, NULL, NULL); - if(0 == rv) { - // Success reading. See if it has a driver. - if(!q.pp.signal) { - // No signal, no writer - Py_INCREF(Py_False); - return Py_False; - } - hal_query_t qs = {}; - qs.name = q.pp.signal; - if(0 != (rv = hal_getref_s(&qs))) { - PyErr_Format(PyExc_NameError, "Signal '%s' of pin '%s' gave unexpected error=%d", q.pp.signal, q.name, rv); - return NULL; - } - return PyBool_FromLong(qs.sig.writers > 0); - } else if(-ENOENT == rv) { - // Pin not found - PyErr_Format(PyExc_NameError, "Pin `%s' does not exist", q.name); - return NULL; + std::optional q; + try { + q = halxx::pin_param_query(name, HAL_QTYPE_PIN); + } catch(const std::system_error &e) { + raise(PyExc_RuntimeError, fmt::format("pin_has_writer: {}: returned error: {}", name, + hal_strerror(-e.code().value()))); + } + if(!q) + raise(PyExc_NameError, fmt::format("Pin `{}' does not exist", name)); + if(!q->pp.signal) + return py::bool_(false); // No signal, no writer + std::string sig = q->pp.signal; + int rv = -ENOENT; + try { + if(auto s = halxx::signal_query(sig)) + return py::bool_(s->sig.writers > 0); + } catch(const std::system_error &e) { + rv = -e.code().value(); + } + raise(PyExc_NameError, fmt::format("Signal '{}' of pin '{}' gave unexpected error={}", sig, name, rv)); +} + +static py::object component_exists(const std::string &name) +{ + try { + return py::bool_(halxx::component_query(name).has_value()); + } catch(const std::system_error &e) { + raise(PyExc_RuntimeError, fmt::format("component_exists: hal_comp_by_name '{}' returned {}", + name, -e.code().value())); } - PyErr_Format(PyExc_RuntimeError, "pin_has_writer: %s: returned error: %s", q.name, hal_strerror(rv)); - return NULL; } - -PyObject *component_exists(PyObject * /*self*/, PyObject *args) { - const char *name; - if(!PyArg_ParseTuple(args, "s", &name)) return NULL; - - int rv = hal_comp_by_name(name, NULL); - if(0 == rv) { - Py_INCREF(Py_True); - return Py_True; - } else if(-ENOENT == rv) { - Py_INCREF(Py_False); - return Py_False; +static py::object component_is_ready(const std::string &name) +{ + try { + return py::bool_(halxx::component_is_ready(name)); + } catch(const std::system_error &) { + return py::bool_(false); } - PyErr_Format(PyExc_RuntimeError, "component_exists: hal_comp_by_name '%s' returned %d", name, rv); - return NULL; } -PyObject *component_is_ready(PyObject * /*self*/, PyObject *args) { - const char *name; - if(!PyArg_ParseTuple(args, "s", &name)) return NULL; - - hal_query_t q = {}; - int rv = hal_comp_by_name(name, &q); - return PyBool_FromLong(0 == rv && q.comp.ready); +static void set_msg_level(int level) +{ + int res = rtapi_set_msg_level(level); + if(res) + raise_hal_errno(-res); } -PyObject *new_sig(PyObject * /*self*/, PyObject *args) { - const char *name; - int type,retval; - if(!PyArg_ParseTuple(args, "si", &name,&type)) return NULL; - - //printf("INFO HALMODULE -- make signal -> %s type %d\n",name,(hal_type_t) type); - switch (type) { +// new_sig(), connect() and disconnect() return True when the library +// call failed, as they always did. +static py::object new_sig(const std::string &name, int type) +{ + switch(type) { case HAL_BOOL: case HAL_SINT: case HAL_UINT: case HAL_REAL: case HAL_PORT: - retval = hal_signal_new(name, (hal_type_t)type); - break; - default: { - PyErr_Format(PyExc_RuntimeError, "not a valid HAL signal type"); - return NULL; - } - } - return PyBool_FromLong(retval != 0); -} - -PyObject *connect(PyObject * /*self*/, PyObject *args) { - const char *signame,*pinname; - if(!PyArg_ParseTuple(args, "ss", &pinname,&signame)) return NULL; - - //printf("INFO HALMODULE -- link sig %s to pin %s\n",signame,pinname); - return PyBool_FromLong(hal_link(pinname, signame) != 0); -} - -PyObject *disconnect(PyObject * /*self*/, PyObject *args) { - const char *pinname; - if(!PyArg_ParseTuple(args, "s", &pinname)) return NULL; - - //printf("INFO HALMODULE -- unlink pin %s\n",pinname); - return PyBool_FromLong(hal_unlink(pinname) != 0); -} - -static int set_common_cb(hal_query_t *q, void *arg) -{ - PyObject *obj = static_cast(arg); - hal_type_t type = HAL_QTYPE_SIGNAL == q->qtype ? q->sig.type : q->pp.type; - hal_query_value_u *qvp = HAL_QTYPE_SIGNAL == q->qtype ? &q->sig.value : &q->pp.value; - - switch(type) { - case HAL_BOOL: { - bool tmp; - if(!from_python(obj, &tmp)) return -EINVAL; - qvp->b = tmp; - break; - } - case HAL_REAL: { - rtapi_real tmp; - if(!from_python(obj, &tmp)) return -EINVAL; - qvp->r = tmp; - break; - } - case HAL_SINT: { - rtapi_sint tmp; - if(!from_python(obj, &tmp)) return -EINVAL; - qvp->s = tmp; - break; - } - case HAL_UINT: { - rtapi_uint tmp; - if(!from_python(obj, &tmp)) return -EINVAL; - qvp->u = tmp; - break; - } - case HAL_PORT: { - unsigned tmp; - if(!from_python(obj, &tmp)) return -EINVAL; - qvp->u = tmp; - break; - } + return py::bool_(0 != halxx::signal_new(name, (hal_type_t)type)); default: - // Shouldn't get here, but just in case... - return -EBADF; - } - return 0; -} - -PyObject *set_p(PyObject * /*self*/, PyObject *args) { - hal_query_t q = {}; - PyObject *obj; - - if(!PyArg_ParseTuple(args, "sO", &q.name, &obj)) return NULL; - - int rv = hal_set_p(&q, set_common_cb, obj); - if(rv < 0) { - PyErr_Format(PyExc_RuntimeError, "set_p: %s: %s", q.name, hal_strerror(rv)); - return NULL; + raise(PyExc_RuntimeError, "not a valid HAL signal type"); } - return PyBool_FromLong(1); } -PyObject *set_s(PyObject * /*self*/, PyObject *args) { - hal_query_t q = {}; - PyObject *obj; - - if(!PyArg_ParseTuple(args, "sO", &q.name, &obj)) return NULL; - - int rv = hal_set_s(&q, set_common_cb, obj); - if(rv < 0) { - PyErr_Format(PyExc_RuntimeError, "set_s: %s: %s", q.name, hal_strerror(rv)); - return NULL; - } - return PyBool_FromLong(1); +static py::object connect(const std::string &pinname, const std::string &signame) +{ + return py::bool_(0 != halxx::link(pinname, signame)); } -/*######################################*/ -/* Get a Pin, Param or signal value */ -/* Search order: pin, param, signal */ - -static PyObject *halref_to_object(hal_type_t type, const hal_query_value_u *v) +static py::object disconnect(const std::string &pinname) { - switch(type) { - case HAL_BOOL: return to_python(v->b); - case HAL_UINT: return to_python(v->u); - case HAL_SINT: return to_python(v->s); - case HAL_REAL: return to_python(v->r); - case HAL_PORT: return to_python(v->s); - default: - PyErr_Format(PyExc_RuntimeError, "halref_to_object: unknown hal type '%d'", (int)type); - return NULL; - } + return py::bool_(0 != halxx::unlink(pinname)); } -// -// get_p() tries to get the value of a named pin or parameter. -// -PyObject *get_p(PyObject * /*self*/, PyObject *args) +// set_p() and set_s() look the item up for its type, convert the value +// to it and hand it to the library. Any failure is a RuntimeError with +// the library's description. +static py::object set_p(const std::string &name, py::handle obj) { - hal_query_t q = {}; - - if(!PyArg_ParseTuple(args, "s", &q.name)) return NULL; - - // A connected pin will return the signal's value - int rv = hal_get_p(&q, NULL, NULL); - if(0 == rv) - return halref_to_object(q.pp.type, &q.pp.value); - if(-ENOENT == rv) { - Py_INCREF(Py_None); - return Py_None; + std::optional q; + try { + q = halxx::pin_param_query(name); + } catch(const std::system_error &e) { + raise_query("set_p", name, e); + } + if(!q) + raise(PyExc_RuntimeError, fmt::format("set_p: {}: {}", name, hal_strerror(-ENOENT))); + halxx::value_t v; + try { + v = value_from_python(q->pp.type, obj.ptr()); + } catch(const py::error_already_set &) { + raise(PyExc_RuntimeError, fmt::format("set_p: {}: {}", name, hal_strerror(-EINVAL))); + } + try { + halxx::set_value(name, v); + } catch(const std::out_of_range &) { + raise(PyExc_RuntimeError, fmt::format("set_p: {}: {}", name, hal_strerror(-ERANGE))); + } catch(const std::system_error &e) { + raise_query("set_p", name, e); + } + return py::bool_(true); +} + +static py::object set_s(const std::string &name, py::handle obj) +{ + std::optional q; + try { + q = halxx::signal_query(name); + } catch(const std::system_error &e) { + raise_query("set_s", name, e); + } + if(!q) + raise(PyExc_RuntimeError, fmt::format("set_s: {}: {}", name, hal_strerror(-ENOENT))); + halxx::value_t v; + try { + v = value_from_python(q->sig.type, obj.ptr()); + } catch(const py::error_already_set &) { + raise(PyExc_RuntimeError, fmt::format("set_s: {}: {}", name, hal_strerror(-EINVAL))); + } + try { + halxx::set_signal(name, v); + } catch(const std::out_of_range &) { + raise(PyExc_RuntimeError, fmt::format("set_s: {}: {}", name, hal_strerror(-ERANGE))); + } catch(const std::system_error &e) { + raise_query("set_s", name, e); + } + return py::bool_(true); +} + +// get_p() and get_s() return None when there is no such item; a +// connected pin reads its signal's value. A port pin reads 0, a port +// signal its buffer size. +static py::object get_p(const std::string &name) +{ + try { + if(auto q = halxx::pin_param_query(name)) + return ref_value(q->pp.type, q->pp.ref, false); + } catch(const std::system_error &e) { + raise_query("get_p", name, e); } - // Get here: most likely a serious error - PyErr_Format(PyExc_RuntimeError, "get_p: %s: %s", q.name, hal_strerror(rv)); - return NULL; + return py::none(); } -// -// get_s() tries to get the value of a named signal. -// -PyObject *get_s(PyObject * /*self*/, PyObject *args) +static py::object get_s(const std::string &name) { - hal_query_t q = {}; - - if(!PyArg_ParseTuple(args, "s", &q.name)) return NULL; - - int rv = hal_get_s(&q, NULL, NULL); - if(0 == rv) - return halref_to_object(q.sig.type, &q.sig.value); - if(-ENOENT == rv) { - Py_INCREF(Py_None); - return Py_None; + try { + if(auto q = halxx::signal_query(name)) + return ref_value(q->sig.type, q->sig.ref, true); + } catch(const std::system_error &e) { + raise_query("get_s", name, e); } - // Get here: most likely a serious error - PyErr_Format(PyExc_RuntimeError, "get_s: %s: %s", q.name, hal_strerror(rv)); - return NULL; + return py::none(); } -// -// get_value() first tries pins/params and then signals if no pin/param with -// that name. -// -PyObject *get_value(PyObject * /*self*/, PyObject *args) +// get_value() tries pins and params, then signals, and raises when +// there is neither. +static py::object get_value(const std::string &name) { - hal_query_t q = {}; - - if(!PyArg_ParseTuple(args, "s", &q.name)) return NULL; - - // Try a pin/param - // A connected pin will return the signal's value - int rv = hal_get_p(&q, NULL, NULL); - if(0 == rv) - return halref_to_object(q.pp.type, &q.pp.value); - // No pin/param with that name, try a signal - if(-ENOENT == rv) { - rv = hal_get_s(&q, NULL, NULL); - if(0 == rv) - return halref_to_object(q.sig.type, &q.sig.value); + try { + if(auto q = halxx::pin_param_query(name)) + return ref_value(q->pp.type, q->pp.ref, false); + if(auto q = halxx::signal_query(name)) + return ref_value(q->sig.type, q->sig.ref, true); + } catch(const std::system_error &e) { + raise_query("get_value", name, e); } - PyErr_Format(PyExc_RuntimeError, "get_value: %s: %s", q.name, hal_strerror(rv)); - return NULL; + raise(PyExc_RuntimeError, fmt::format("get_value: {}: {}", name, hal_strerror(-ENOENT))); } -/*######################################*/ -/* Get a dict of pin info for all pins in system */ -static int pinparaminfo_add(const hal_query_t *q, PyObject *lst) +static bool known_type(hal_type_t t) { - PyObject *obj; - static const char str_n[] = "NAME"; - static const char str_v[] = "VALUE"; - static const char str_t[] = "TYPE"; - static const char str_d[] = "DIRECTION"; - - switch(q->pp.type) { - case HAL_BOOL: - obj = Py_BuildValue("{s:s,s:N,s:N,s:N}", - str_n, q->name, str_v, PyBool_FromLong(hal_get_bool(q->pp.ref.b)), - str_d, PyLong_FromLong(q->pp.dir), str_t, PyLong_FromLong(HAL_BOOL)); - break; - case HAL_UINT: - obj = Py_BuildValue("{s:s,s:K,s:N,s:N}", - str_n, q->name, str_v, hal_get_uint(q->pp.ref.u), - str_d, PyLong_FromLong(q->pp.dir), str_t, PyLong_FromLong(HAL_UINT)); - break; - case HAL_SINT: - obj = Py_BuildValue("{s:s,s:L,s:N,s:N}", - str_n, q->name, str_v, hal_get_sint(q->pp.ref.s), - str_d, PyLong_FromLong(q->pp.dir), str_t, PyLong_FromLong(HAL_SINT)); - break; - case HAL_REAL: - obj = Py_BuildValue("{s:s,s:d,s:N,s:N}", - str_n, q->name, str_v, hal_get_real(q->pp.ref.r), - str_d, PyLong_FromLong(q->pp.dir), str_t, PyLong_FromLong(HAL_REAL)); - break; - case HAL_PORT: - obj = Py_BuildValue("{s:s,s:l,s:N,s:N}", - str_n, q->name, str_v, hal_get_sint(q->pp.ref.s), - str_d, PyLong_FromLong(q->pp.dir), str_t, PyLong_FromLong(HAL_PORT)); - break; - default: - obj = Py_BuildValue("{s:s,s:s,s:N,s:s}", - str_n, q->name, str_v, NULL, - str_d, PyLong_FromLong(q->pp.dir), str_t, NULL); - break; - } - PyList_Append(lst, obj); - Py_DECREF(obj); - return 0; + return HAL_BOOL == t || HAL_SINT == t || HAL_UINT == t || HAL_REAL == t || HAL_PORT == t; } -PyObject *get_info_pins(PyObject * /*self*/, PyObject * /*args*/) +// A list of {NAME, VALUE, DIRECTION, TYPE} dicts for all pins or params. +static py::list get_info_pp(bool ispin) { - HalQRec qrec(1024); // We normally have many pins + HalQRec qrec(ispin ? 1024 : 512); // We normally have many of them hal_query_t q = {}; - q.qtype = HAL_QTYPE_PIN; // Only handle pins + q.qtype = ispin ? HAL_QTYPE_PIN : HAL_QTYPE_PARAM; int rv = hal_list_p(&q, HalQRec::get_qrec_cb, &qrec); - if(0 != rv) { - PyErr_Format(PyExc_RuntimeError, "hal_list_p: returned '%s' (%d)", hal_strerror(rv), rv); - return NULL; - } + if(0 != rv) + raise(PyExc_RuntimeError, fmt::format("hal_list_p: returned '{}' ({})", hal_strerror(rv), rv)); - PyObject* python_list = PyList_New(0); + py::list lst; for(size_t i = 0; i < qrec.size(); i++) { - pinparaminfo_add(qrec.rec(i), python_list); + const hal_query_t *r = qrec.rec(i); + bool known = known_type(r->pp.type); + py::dict d; + d["NAME"] = r->name; + d["VALUE"] = ref_value(r->pp.type, r->pp.ref, ispin); + d["DIRECTION"] = py::int_((int)r->pp.dir); + d["TYPE"] = known ? py::object(py::int_((int)r->pp.type)) : py::object(py::none()); + lst.append(d); } - return python_list; + return lst; } -/*######################################*/ -/* Get a dict of signal info for all signals in system */ - -static int siginfo_writer_cb(hal_query_t *q, void *arg) +static int siginfo_writer_cb(hal_query_t *q, void *arg) noexcept { if(HAL_OUT == q->pp.dir) { // Found the writer, record and quit the loop @@ -1478,607 +792,40 @@ static int siginfo_writer_cb(hal_query_t *q, void *arg) return 0; } -static int siginfo_add(const hal_query_t *q, PyObject *lst) -{ - PyObject *obj; - static const char str_n[] = "NAME"; - static const char str_v[] = "VALUE"; - static const char str_t[] = "TYPE"; - static const char str_d[] = "DRIVER"; - - const char *writer = NULL; - if(q->sig.writers > 0) { - // Retrieve the writer pin name - hal_query_t qd = {}; - qd.name = q->name; - hal_list_p_s(&qd, siginfo_writer_cb, (void *)&writer); - } - - switch(q->sig.type) { - case HAL_BOOL: - obj = Py_BuildValue("{s:s,s:N,s:s,s:N}", - str_n, q->name, str_v, PyBool_FromLong(hal_get_bool(q->sig.ref.b)), - str_d, writer, str_t, PyLong_FromLong(HAL_BOOL)); - break; - case HAL_UINT: - obj = Py_BuildValue("{s:s,s:K,s:s,s:N}", - str_n, q->name, str_v, hal_get_uint(q->sig.ref.u), - str_d, writer, str_t, PyLong_FromLong(HAL_UINT)); - break; - case HAL_SINT: - obj = Py_BuildValue("{s:s,s:L,s:s,s:N}", - str_n, q->name, str_v, hal_get_sint(q->sig.ref.s), - str_d, writer, str_t, PyLong_FromLong(HAL_SINT)); - break; - case HAL_REAL: - obj = Py_BuildValue("{s:s,s:d,s:s,s:N}", - str_n, q->name, str_v, hal_get_real(q->sig.ref.r), - str_d, writer, str_t, PyLong_FromLong(HAL_REAL)); - break; - case HAL_PORT: - obj = Py_BuildValue("{s:s,s:l,s:s,s:N}", - str_n, q->name, str_v, hal_get_sint(q->sig.ref.s), - str_d, writer, str_t, PyLong_FromLong(HAL_PORT)); - break; - default: - obj = Py_BuildValue("{s:s,s:s,s:s,s:s}", - str_n, q->name, str_v, NULL, - str_d, writer, str_t, NULL); - break; - } - PyList_Append(lst, obj); - Py_DECREF(obj); - return 0; -} - -PyObject *get_info_signals(PyObject * /*self*/, PyObject * /*args*/) +// A list of {NAME, VALUE, DRIVER, TYPE} dicts for all signals. +static py::list get_info_signals(py::args) { HalQRec qrec(256); // We normally have many signals hal_query_t q = {}; int rv = hal_list_s(&q, HalQRec::get_qrec_cb, &qrec); - if(0 != rv) { - PyErr_Format(PyExc_RuntimeError, "hal_list_s: returned '%s' (%d)", hal_strerror(rv), rv); - return NULL; - } - - PyObject* python_list = PyList_New(0); - for(size_t i = 0; i < qrec.size(); i++) { - siginfo_add(qrec.rec(i), python_list); - } - return python_list; -} - -/*######################################*/ -/* Get a dict of parameter info for all parameters in system */ -PyObject *get_info_params(PyObject * /*self*/, PyObject * /*args*/) -{ - HalQRec qrec(512); // We normally have many parameters - hal_query_t q = {}; - q.qtype = HAL_QTYPE_PARAM; // Only handle parameters - int rv = hal_list_p(&q, HalQRec::get_qrec_cb, &qrec); - if(0 != rv) { - PyErr_Format(PyExc_RuntimeError, "hal_list_p: returned '%s' (%d)", hal_strerror(rv), rv); - return NULL; - } + if(0 != rv) + raise(PyExc_RuntimeError, fmt::format("hal_list_s: returned '{}' ({})", hal_strerror(rv), rv)); - PyObject* python_list = PyList_New(0); + py::list lst; for(size_t i = 0; i < qrec.size(); i++) { - pinparaminfo_add(qrec.rec(i), python_list); - } - return python_list; -} - -static PyObject *pyhal_get_realtime_type(PyObject * /*self*/, PyObject * /*o*/) { - int res = hal_get_realtime_type(); - - // Get an IntEnum _hal.RTType.X instance from the result. - // This may be slower than a cached module/IntEnum ref, but this is - // normally only called once. - PyObject *m = PyImport_ImportModule("_hal"); - if(!m) - return NULL; - PyObject *cls = PyObject_GetAttrString(m, "RTType"); - Py_DECREF(m); - if(!cls) - return NULL; - PyObject *e = PyObject_CallFunction(cls, "l", (long)res); - Py_DECREF(cls); - return e; -} - -static PyObject *pyhal_is_initialized(PyObject * /*self*/, PyObject * /*o*/) { - return PyBool_FromLong(hal_is_init()); -} - -struct shmobject { - PyObject_HEAD - halobject *comp; - int key; - int shm_id; - unsigned long size; - void *buf; -}; - -static int pyshm_init(PyObject *_self, PyObject *args, PyObject * /*kw*/) { - shmobject *self = reinterpret_cast(_self); - self->comp = NULL; - self->shm_id = -1; - - rtapi_print_msg(RTAPI_MSG_ERR, "halmodule: hal.shm has been deprecated.\n"); - - if(!PyArg_ParseTuple(args, "O!ik", &halobject_type, &self->comp, &self->key, &self->size)) - return -1; - - self->shm_id = rtapi_shmem_new(self->key, self->comp->hal_id, self->size); - if(self->shm_id < 0) { - self->comp = NULL; - self->size = 0; - pyrtapi_error(self->shm_id); - return -1; - } - - rtapi_shmem_getptr(self->shm_id, &self->buf); - Py_INCREF(self->comp); - - return 0; -} - -static void pyshm_delete(PyObject *_self) { - shmobject *self = reinterpret_cast(_self); - if(self->comp && self->shm_id > 0) - rtapi_shmem_delete(self->shm_id, self->comp->hal_id); - Py_XDECREF(self->comp); -} - -static int shm_buffer_getbuffer(PyObject *obj, Py_buffer *view, int /*flags*/) { - if (view == NULL) { - PyErr_SetString(PyExc_ValueError, "NULL view in getbuffer"); - return -1; - } - shmobject* self = reinterpret_cast(obj); - view->obj = reinterpret_cast(self); - view->buf = (void*)self->buf; - view->len = self->size; - view->readonly = 0; - Py_INCREF(self); // need to increase the reference count - return 0; -} - -static PyObject *pyshm_repr(PyObject *_self) { - shmobject *self = reinterpret_cast(_self); - return PyUnicode_FromFormat("", - self->key, self->shm_id, (unsigned long)self->size); -} - -static PyObject *shm_setsize(PyObject *_self, PyObject *args) { - shmobject *self = reinterpret_cast(_self); - if(!PyArg_ParseTuple(args, "k", &self->size)) return NULL; - Py_INCREF(Py_None); - return Py_None; -} - - -static PyObject *shm_getbuffer(PyObject *_self, PyObject * /*dummy*/) { - - shmobject *self = reinterpret_cast(_self); - return (PyObject*)PyMemoryView_FromObject(reinterpret_cast(self)); -} - -static PyObject *set_msg_level(PyObject * /*_self*/, PyObject *args) { - int level, res; - if(!PyArg_ParseTuple(args, "i", &level)) return NULL; - res = rtapi_set_msg_level(level); - if(res) return pyhal_error(res); - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject *get_msg_level(PyObject * /*_self*/, PyObject * /*args*/) { - return PyLong_FromLong(rtapi_get_msg_level()); -} - -static PyBufferProcs shmbuffer_procs = { - (getbufferproc)shm_buffer_getbuffer, /* bf_getbuffer */ - (releasebufferproc)NULL, //(releasebufferproc)shm_buffer_releasebuffer, /* bf_releasebuffer */ -}; - -static PyMethodDef shm_methods[] = { - {"getbuffer", shm_getbuffer, METH_NOARGS, - "Get a writable buffer object for the shared memory segment"}, - {"setsize", shm_setsize, METH_VARARGS, - "Set the size of the shared memory segment"}, - {}, -}; - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant" -static -PyTypeObject shm_type = { - PyVarObject_HEAD_INIT(NULL, 0) - "hal.shm", /*tp_name*/ - sizeof(shmobject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - (destructor)pyshm_delete, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - (reprfunc)pyshm_repr, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - &shmbuffer_procs, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - "HAL Shared Memory", /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - shm_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ - 0, /*tp_base*/ - 0, /*tp_dict*/ - 0, /*tp_descr_get*/ - 0, /*tp_descr_set*/ - 0, /*tp_dictoffset*/ - (initproc)pyshm_init, /*tp_init*/ - 0, /*tp_alloc*/ - PyType_GenericNew, /*tp_new*/ - 0, /*tp_free*/ - 0, /*tp_is_gc*/ - 0, /*tp_bases*/ - 0, /*tp_mro*/ - 0, /*tp_cache*/ - 0, /*tp_subclasses*/ - 0, /*tp_weaklink*/ - 0, /*tp_del*/ - 0, /*tp_version_tag*/ - 0, /*tp_finalize*/ -#if PY_VERSION_HEX >= 0x030800f0 // 3.8 - 0, /*tp_vectorcall*/ -#if PY_VERSION_HEX >= 0x030c00f0 // 3.12 - 0, /*tp_watched*/ -#if PY_VERSION_HEX >= 0x030d00f0 // 3.13 - 0, /*tp_versions_used*/ -#endif -#endif -#endif -}; -#pragma GCC diagnostic pop - -struct streamobj { - PyObject_HEAD - hal_stream_t stream; - PyObject *pyelt; - halobject *comp; - int key; - bool creator; - unsigned sampleno; -}; - -static int pystream_init(PyObject *_self, PyObject *args, PyObject * /*kw*/) { - int depth=0; - const char *typestring=NULL; - - streamobj *self = reinterpret_cast(_self); - self->sampleno = 0; - - // creating a new stream - int r; - if(PyTuple_GET_SIZE(args) == 4) - r = PyArg_ParseTuple(args, "O!iis:hal.stream", - &halobject_type, &self->comp, &self->key, &depth, &typestring); - else - r = PyArg_ParseTuple(args, "O!i|s:hal.stream", - &halobject_type, &self->comp, &self->key, &typestring); - - if(!r) return -1; - - Py_XINCREF(self->comp); - - if(depth) { - self->creator = 1; - r = hal_stream_create(&self->stream, self->comp->hal_id, - self->key, depth, typestring); - } else { - self->creator = 0; - r = hal_stream_attach(&self->stream, self->comp->hal_id, self->key, typestring); - } - if(r < 0) { errno = -r; PyErr_SetFromErrno(PyExc_IOError); return -1; } - - int n = hal_stream_element_count(&self->stream); - PyObject *t = PyBytes_FromStringAndSize(NULL, n); - if(!t) { - if(self->creator) - hal_stream_destroy(&self->stream); - else - hal_stream_detach(&self->stream); - return -1; - } - - char *tbuf = PyBytes_AsString(t); - - for(int i=0; istream, i)) { - case HAL_BOOL: tbuf[i] = 'b'; break; - case HAL_REAL: tbuf[i] = 'f'; break; - case HAL_SINT: tbuf[i] = 's'; break; - case HAL_UINT: tbuf[i] = 'u'; break; - default: tbuf[i] = '?'; break; - } - } - self->pyelt = t; - - return 0; -} - -PyObject *stream_read(PyObject *_self, PyObject * /*unused*/) { - streamobj *self = reinterpret_cast(_self); - int n = PyBytes_Size(self->pyelt); - if(n <= 0) { - Py_INCREF(Py_None); - return Py_None; - } - std::vector buf(n); - if(hal_stream_read(&self->stream, buf.data(), &self->sampleno) < 0) { - Py_INCREF(Py_None); - return Py_None; - } - - PyObject *r = PyTuple_New(n); - if(!r) return NULL; - - for(int i=0; ipyelt)[i]) { - case 'B': - case 'b': o = to_python(buf[i].b); break; - case 'R': - case 'r': - case 'F': - case 'f': o = to_python(buf[i].f); break; - case 'S': - case 's': o = to_python(buf[i].s); break; - case 'U': - case 'u': o = to_python(buf[i].u); break; - case 'L': - case 'l': o = to_python(buf[i].l); break; - case 'K': - case 'k': o = to_python(buf[i].k); break; - default: Py_INCREF(Py_None); o = Py_None; break; - } - if(!o) { - Py_DECREF(r); - return NULL; - } - PyTuple_SET_ITEM(r, i, o); - } - return r; -} - -PyObject *stream_write(PyObject *_self, PyObject *args) { - streamobj *self = reinterpret_cast(_self); - PyObject *data; - if(!PyArg_ParseTuple(args, "O!:hal.stream.write", &PyTuple_Type, &data)) - return NULL; - - int n = PyBytes_Size(self->pyelt); - if(n < PyTuple_GET_SIZE(data)) { - PyErr_SetString(PyExc_ValueError, "Too few elements to unpack"); - return NULL; - } - if(n > PyTuple_GET_SIZE(data)) { - PyErr_SetString(PyExc_ValueError, "Too many elements to unpack"); - return NULL; - } - - std::vector buf(n); - for(int i=0; ipyelt)[i]) { - case 'B': - case 'b': if(!from_python(o, &buf[i].b)) return NULL; break; - case 'R': - case 'r': - case 'F': - case 'f': if(!from_python(o, &buf[i].f)) return NULL; break; - case 'S': - case 's': if(!from_python(o, &buf[i].s)) return NULL; break; - case 'U': - case 'u': if(!from_python(o, &buf[i].u)) return NULL; break; - case 'L': - case 'l': if(!from_python(o, &buf[i].l)) return NULL; break; - case 'K': - case 'k': if(!from_python(o, &buf[i].k)) return NULL; break; - default: memset(&buf[i], 0, sizeof(buf[i])); break; + const hal_query_t *r = qrec.rec(i); + const char *writer = nullptr; + if(r->sig.writers > 0) { + // Retrieve the writer pin name + hal_query_t qd = {}; + qd.name = r->name; + hal_list_p_s(&qd, siginfo_writer_cb, (void *)&writer); } + bool known = known_type(r->sig.type); + py::dict d; + d["NAME"] = r->name; + d["VALUE"] = ref_value(r->sig.type, r->sig.ref, true); + d["DRIVER"] = writer ? py::object(py::str(writer)) : py::object(py::none()); + d["TYPE"] = known ? py::object(py::int_((int)r->sig.type)) : py::object(py::none()); + lst.append(d); } - int r = hal_stream_write(&self->stream, buf.data()); - if(r < 0) { - errno = -r; PyErr_SetFromErrno(PyExc_IOError); return NULL; - } - Py_INCREF(Py_None); - return Py_None; -} - -static PyMethodDef stream_methods[] = { - {"read", stream_read, METH_NOARGS, NULL}, - {"write", stream_write, METH_VARARGS, NULL}, - {} -}; - -#define VFC(f) reinterpret_cast(f) - -template -PyObject *stream_getter(PyObject *_self, void *vfp) { - streamobj *self = reinterpret_cast(_self); - typedef T (*F)(hal_stream_t*); - F fn = reinterpret_cast(vfp); - T result = fn(&self->stream); - return to_python(result); + return lst; } -PyObject *stream_element_types(PyObject *_self, void * /*unused*/) { - streamobj *self = reinterpret_cast(_self); - if(!self->pyelt) { - PyErr_Format(PyExc_RuntimeError, "Stream type-string object was not set"); - return NULL; - } - Py_INCREF(self->pyelt); - return self->pyelt; -} - -static PyMemberDef stream_members[] = { - {"sampleno", T_UINT, offsetof(streamobj, sampleno), READONLY, - "The number of the last successfully read sample"}, - {} -}; - -static PyGetSetDef stream_getset[] = { - {"readable", stream_getter, NULL, NULL, VFC(hal_stream_readable)}, - {"writable", stream_getter, NULL, NULL, VFC(hal_stream_writable)}, - {"depth", stream_getter, NULL, NULL, VFC(hal_stream_depth)}, - {"element_types", stream_element_types, NULL, NULL, NULL}, - {"maxdepth", stream_getter, NULL, NULL, VFC(hal_stream_maxdepth)}, - {"num_underruns", stream_getter, NULL, NULL, VFC(hal_stream_num_underruns)}, - {"num_overruns", stream_getter, NULL, NULL, VFC(hal_stream_num_overruns)}, - {} -}; - -static void pystream_delete(PyObject *_self) { - streamobj *self = reinterpret_cast(_self); - if(self->creator) - hal_stream_destroy(&self->stream); - else - hal_stream_detach(&self->stream); - Py_XDECREF(self->pyelt); - Py_XDECREF(self->comp); - Py_TYPE(self)->tp_free(self); -} - -static PyObject *pystream_repr(PyObject *_self) { - streamobj *self = reinterpret_cast(_self); - return PyUnicode_FromFormat("", self->key, - self->creator ? " creator" : ""); -} - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant" -static -PyTypeObject stream_type = { - PyVarObject_HEAD_INIT(NULL, 0) - "hal.stream", /*tp_name*/ - sizeof(streamobj), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - pystream_delete, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - pystream_repr, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - "HAL Stream", /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - stream_methods, /*tp_methods*/ - stream_members, /*tp_members*/ - stream_getset, /*tp_getset*/ - 0, /*tp_base*/ - 0, /*tp_dict*/ - 0, /*tp_descr_get*/ - 0, /*tp_descr_set*/ - 0, /*tp_dictoffset*/ - pystream_init, /*tp_init*/ - 0, /*tp_alloc*/ - PyType_GenericNew, /*tp_new*/ - 0, /*tp_free*/ - 0, /*tp_is_gc*/ - 0, /*tp_bases*/ - 0, /*tp_mro*/ - 0, /*tp_cache*/ - 0, /*tp_subclasses*/ - 0, /*tp_weaklink*/ - 0, /*tp_del*/ - 0, /*tp_version_tag*/ - 0, /*tp_finalize*/ -#if PY_VERSION_HEX >= 0x030800f0 // 3.8 - 0, /*tp_vectorcall*/ -#if PY_VERSION_HEX >= 0x030c00f0 // 3.12 - 0, /*tp_watched*/ -#if PY_VERSION_HEX >= 0x030d00f0 // 3.13 - 0, /*tp_versions_used*/ -#endif -#endif -#endif -}; -#pragma GCC diagnostic pop - - -static PyMethodDef module_methods[] = { - {"pin_has_writer", pin_has_writer, METH_VARARGS, - ".pin_has_writer('pin_name'): Return a FALSE value if a pin has no writers and TRUE if it does"}, - {"component_exists", component_exists, METH_VARARGS, - ".component_exists('component_name'): Return a True value if the named component exists"}, - {"component_is_ready", component_is_ready, METH_VARARGS, - ".component_is_ready('component_name'): Return a True value if the named component is ready"}, - {"set_msg_level", set_msg_level, METH_VARARGS, - ".set_msg_level(level): Set the RTAPI message level"}, - {"get_msg_level", get_msg_level, METH_NOARGS, - ".get_msg_level(): Get the RTAPI message level"}, - {"new_sig", new_sig, METH_VARARGS, - ".new_sig('signal_name', type): Create a new signal with the specified name. 'type' is one of hal.Type.[BOOL,REAL,SINT,UINT]."}, - {"connect", connect, METH_VARARGS, - ".connect('pin_name', 'signal_name'): Connect the named pin to the named signal."}, - {"disconnect", disconnect, METH_VARARGS, - ".disconnect('pin_name'): Disconnect the named pin from any signal."}, - - {"set_p", set_p, METH_VARARGS, - ".set_p('name', 'value'): Set the pin or param value"}, - {"set_s", set_s, METH_VARARGS, - ".set_s('name', 'value'): Set the signal value"}, - {"get_p", get_p, METH_VARARGS, - ".get_p('name', 'value'): Get the pin or param value. Will return the signal value if it is a pin and connected"}, - {"get_s", get_s, METH_VARARGS, - ".get_s('name', 'value'): Get the signal value"}, - - - {"get_value", get_value, METH_VARARGS, - ".get_value('name'): Gets the pin, param or signal value"}, - {"get_info_pins", get_info_pins, METH_VARARGS, - ".get_info_pins(): Get a list of dicts for all the pins; {NAME:, VALUE:, DIRECTION:}"}, - {"get_info_signals", get_info_signals, METH_VARARGS, - ".get_info_signals(): Get a list of dicts for all the signals; {NAME:, VALUE:}"}, - {"get_info_params", get_info_params, METH_VARARGS, - ".get_info_params(): Get a list of dicts for all the parameters; {NAME:, VALUE:}"}, - {"get_realtime_type", pyhal_get_realtime_type, METH_NOARGS, - ".get_realtime_type(): Return the type of the running realtime"}, - {"is_initialized", pyhal_is_initialized, METH_NOARGS, - ".is_initialized(): Return true if hal is initialized, false otherwise"}, - {}, -}; - -const char *module_doc = "Interface to LinuxCNC's hal\n" +//---------------------------------------------------------------------- +// Module +//---------------------------------------------------------------------- +static const char module_doc[] = "Interface to LinuxCNC's hal\n" "\n" "This module allows the creation of userspace HAL components in Python.\n" "This includes pins and parameters of the various HAL types.\n" @@ -2104,24 +851,11 @@ const char *module_doc = "Interface to LinuxCNC's hal\n" "KeyboardInterrupt exception will be raised." ; -static struct PyModuleDef hal_moduledef = { - PyModuleDef_HEAD_INIT, /* m_base */ - "_hal", /* m_name */ - module_doc, /* m_doc */ - -1, /* m_size */ - module_methods, /* m_methods */ - NULL, /* m_slots */ - NULL, /* m_traverse */ - NULL, /* m_clear */ - NULL, /* m_free */ -}; - // Member order matters: interactive tools pick the first spelling when // several members share a value, so the table lists the preferred -// spelling of each value first (bool, real, sint, uint, port, s32, u32) -// and the alternatives (s64, u64, and the HAL_* spellings) after. The -// enum module preserves dict order, so the first occurrence of each -// value is the canonical member. +// spelling of each value first and the HAL_* spellings after. The enum +// module preserves dict order, so the first occurrence of each value is +// the canonical member. struct halenum_member_t { const char *name; long value; @@ -2138,7 +872,7 @@ static const halenum_member_t halenum_type_members[] = { {"HAL_SINT", HAL_SINT}, {"HAL_UINT", HAL_UINT}, {"HAL_PORT", HAL_PORT}, - {} + {nullptr, 0} }; static const halenum_member_t halenum_dir_members[] = { @@ -2152,7 +886,7 @@ static const halenum_member_t halenum_dir_members[] = { {"HAL_IO", HAL_IO}, {"HAL_RO", HAL_RO}, {"HAL_RW", HAL_RW}, - {} + {nullptr, 0} }; // Not previously registered, no need to have compat names. @@ -2161,7 +895,7 @@ static const halenum_member_t halenum_comp_members[] = { {"USER", HAL_COMP_TYPE_USER}, {"REALTIME", HAL_COMP_TYPE_REALTIME}, {"OTHER", HAL_COMP_TYPE_OTHER}, - {} + {nullptr, 0} }; // Previously registered, but it is new so we don't do compat names. @@ -2175,152 +909,277 @@ static const halenum_member_t halenum_rt_members[] = { {"LXRT", REALTIME_TYPE_LXRT}, {"XENOMAI", REALTIME_TYPE_XENOMAI}, {"XENOMAI_EVL", REALTIME_TYPE_XENOMAI_EVL}, - {} + {nullptr, 0} }; -// Build an enum.IntEnum subclass from a member table. The class claims +// An enum.IntEnum subclass from a member table. The class claims // __module__ "hal", its public home, so repr() and pickle look right. -// Returns a new reference, or NULL with an exception set. -static PyObject *halenum_build(const char *clsname, const halenum_member_t *members) +static py::object halenum_build(const char *clsname, const halenum_member_t *members) { - PyObject *enummod = PyImport_ImportModule("enum"); - if(!enummod) - return NULL; - PyObject *intenum = PyObject_GetAttrString(enummod, "IntEnum"); - Py_DECREF(enummod); - if(!intenum) - return NULL; - - PyObject *names = PyDict_New(); - if(!names) { - Py_DECREF(intenum); - return NULL; - } - for(size_t i = 0; members[i].name; i++) { - PyObject *v = PyLong_FromLong(members[i].value); - if(!v || PyDict_SetItemString(names, members[i].name, v)) { - Py_XDECREF(v); - Py_DECREF(names); - Py_DECREF(intenum); - return NULL; - } - Py_DECREF(v); - } - - PyObject *args = Py_BuildValue("(sO)", clsname, names); - PyObject *kwargs = Py_BuildValue("{ss}", "module", "hal"); - PyObject *cls = (args && kwargs) ? PyObject_Call(intenum, args, kwargs) : NULL; - Py_XDECREF(args); - Py_XDECREF(kwargs); - Py_DECREF(names); - Py_DECREF(intenum); - return cls; + py::dict names; + for(size_t i = 0; members[i].name; i++) + names[members[i].name] = py::int_(members[i].value); + return py::module_::import("enum").attr("IntEnum")(clsname, names, py::arg("module") = "hal"); } -int halquery_add_submodule(PyObject *); // Not gonna make a header for this - -PyMODINIT_FUNC PyInit__hal(void); -PyMODINIT_FUNC PyInit__hal(void) +PYBIND11_MODULE(_hal, m) { - PyObject *m = PyModule_Create(&hal_moduledef); - if(!m) - return NULL; - - int rv; - if(0 != (rv = hal_lib_init())) { - PyErr_Format(PyExc_ImportError, "Initializing hal_lib returned error=%d", rv); - Py_DECREF(m); - return NULL; - } - + m.doc() = module_doc; + + m.def("pin_has_writer", &pin_has_writer, + ".pin_has_writer('pin_name'): Return a FALSE value if a pin has no writers and TRUE if it does"); + m.def("component_exists", &component_exists, + ".component_exists('component_name'): Return a True value if the named component exists"); + m.def("component_is_ready", &component_is_ready, + ".component_is_ready('component_name'): Return a True value if the named component is ready"); + m.def("set_msg_level", &set_msg_level, + ".set_msg_level(level): Set the RTAPI message level"); + m.def("get_msg_level", []() { return rtapi_get_msg_level(); }, + ".get_msg_level(): Get the RTAPI message level"); + m.def("new_sig", &new_sig, + ".new_sig('signal_name', type): Create a new signal with the specified name. 'type' is one of hal.Type.[BOOL,REAL,SINT,UINT,PORT]."); + m.def("connect", &connect, + ".connect('pin_name', 'signal_name'): Connect the named pin to the named signal."); + m.def("disconnect", &disconnect, + ".disconnect('pin_name'): Disconnect the named pin from any signal."); + m.def("set_p", &set_p, + ".set_p('name', value): Set the pin or param value"); + m.def("set_s", &set_s, + ".set_s('name', value): Set the signal value"); + m.def("get_p", &get_p, + ".get_p('name'): Get the pin or param value. Will return the signal value if it is a pin and connected"); + m.def("get_s", &get_s, + ".get_s('name'): Get the signal value"); + m.def("get_value", &get_value, + ".get_value('name'): Gets the pin, param or signal value"); + m.def("get_info_pins", [](py::args) { return get_info_pp(true); }, + ".get_info_pins(): Get a list of dicts for all the pins; {NAME:, VALUE:, DIRECTION:, TYPE:}"); + m.def("get_info_signals", &get_info_signals, + ".get_info_signals(): Get a list of dicts for all the signals; {NAME:, VALUE:, DRIVER:, TYPE:}"); + m.def("get_info_params", [](py::args) { return get_info_pp(false); }, + ".get_info_params(): Get a list of dicts for all the parameters; {NAME:, VALUE:, DIRECTION:, TYPE:}"); + m.def("get_realtime_type", []() { return rttype_enum((long)hal_get_realtime_type()); }, + ".get_realtime_type(): Return the type of the running realtime"); + m.def("is_initialized", []() { return py::bool_(hal_is_init()); }, + ".is_initialized(): Return true if hal is initialized, false otherwise"); + + // A component, a pin or param of one, or a stream needs the HAL shared + // memory, and so do all the by-name functions; map it now. + int rv = hal_lib_init(); + if(0 != rv) + throw std::runtime_error(fmt::format("Initializing hal_lib returned error={}", rv)); Py_AtExit(hal_lib_exit); - pyhal_error_type = PyErr_NewException("hal.error", NULL, NULL); - PyModule_AddObject(m, "error", pyhal_error_type); - - PyType_Ready(&halobject_type); - PyType_Ready(&shm_type); - PyType_Ready(&halpin_type); - PyType_Ready(&stream_type); - PyModule_AddObject(m, "component", (PyObject*)&halobject_type); - PyModule_AddObject(m, "shm", (PyObject*)&shm_type); - PyModule_AddObject(m, "item", (PyObject*)&halpin_type); - PyModule_AddObject(m, "stream", (PyObject*)&stream_type); - - PyModule_AddIntConstant(m, "MSG_NONE", RTAPI_MSG_NONE); - PyModule_AddIntConstant(m, "MSG_ERR", RTAPI_MSG_ERR); - PyModule_AddIntConstant(m, "MSG_WARN", RTAPI_MSG_WARN); - PyModule_AddIntConstant(m, "MSG_INFO", RTAPI_MSG_INFO); - PyModule_AddIntConstant(m, "MSG_DBG", RTAPI_MSG_DBG); - PyModule_AddIntConstant(m, "MSG_ALL", RTAPI_MSG_ALL); - - PyModule_AddIntConstant(m, "HAL_BOOL", HAL_BOOL); - PyModule_AddIntConstant(m, "HAL_REAL", HAL_REAL); - PyModule_AddIntConstant(m, "HAL_SINT", HAL_SINT); - PyModule_AddIntConstant(m, "HAL_UINT", HAL_UINT); - PyModule_AddIntConstant(m, "HAL_PORT", HAL_PORT); - - PyModule_AddIntConstant(m, "HAL_RO", HAL_RO); - PyModule_AddIntConstant(m, "HAL_RW", HAL_RW); - PyModule_AddIntConstant(m, "HAL_IN", HAL_IN); - PyModule_AddIntConstant(m, "HAL_OUT", HAL_OUT); - PyModule_AddIntConstant(m, "HAL_IO", HAL_IO); - - // IntEnum tagging classes for type and direction, built from the - // hal.h constants (see halenum.hh). Registered here so that every - // consumer, Python or C++, shares the same two classes. - PyObject *eobj = halenum_build("Type", halenum_type_members); - if(!eobj || PyModule_AddObject(m, "Type", eobj)) { - Py_XDECREF(eobj); - Py_DECREF(m); - return NULL; - } - eobj = halenum_build("Dir", halenum_dir_members); - if(!eobj || PyModule_AddObject(m, "Dir", eobj)) { - Py_XDECREF(eobj); - Py_DECREF(m); - return NULL; - } - eobj = halenum_build("CompType", halenum_comp_members); - if(!eobj || PyModule_AddObject(m, "CompType", eobj)) { - Py_XDECREF(eobj); - Py_DECREF(m); - return NULL; - } - eobj = halenum_build("RTType", halenum_rt_members); - if(!eobj || PyModule_AddObject(m, "RTType", eobj)) { - Py_XDECREF(eobj); - Py_DECREF(m); - return NULL; - } - - PyModule_AddIntConstant(m, "REALTIME_TYPE_UNINITIALIZED", REALTIME_TYPE_UNINITIALIZED); - PyModule_AddIntConstant(m, "REALTIME_TYPE_NONE", REALTIME_TYPE_NONE); - PyModule_AddIntConstant(m, "REALTIME_TYPE_UNKNOWN", REALTIME_TYPE_UNKNOWN); - PyModule_AddIntConstant(m, "REALTIME_TYPE_PREEMPT_DYNAMIC", REALTIME_TYPE_PREEMPT_DYNAMIC); - PyModule_AddIntConstant(m, "REALTIME_TYPE_PREEMPT_RT", REALTIME_TYPE_PREEMPT_RT); - PyModule_AddIntConstant(m, "REALTIME_TYPE_RTAI", REALTIME_TYPE_RTAI); - PyModule_AddIntConstant(m, "REALTIME_TYPE_LXRT", REALTIME_TYPE_LXRT); - PyModule_AddIntConstant(m, "REALTIME_TYPE_XENOMAI", REALTIME_TYPE_XENOMAI); - PyModule_AddIntConstant(m, "REALTIME_TYPE_XENOMAI_EVL", REALTIME_TYPE_XENOMAI_EVL); - - PyModule_AddIntConstant(m, "is_kernelspace", rtapi_is_kernelspace()); - PyModule_AddIntConstant(m, "is_userspace", !rtapi_is_kernelspace()); - - PyModule_AddIntConstant(m, "streamer_base", 0x48535430); - PyModule_AddIntConstant(m, "sampler_base", 0x48534130); + pyhal_error_type = PyErr_NewException("hal.error", nullptr, nullptr); + if(!pyhal_error_type) + throw py::error_already_set(); + m.attr("error") = py::handle(pyhal_error_type); + + py::class_ comp(m, "component", "HAL Component"); + comp_type = (PyTypeObject *)comp.ptr(); + comp.attr("__module__") = "hal"; + comp + // hal.component(name[, prefix]). Keyword arguments are ignored. + .def(py::init([](py::args args, py::kwargs) { + size_t n = args.size(); + if(n < 1) + raise(PyExc_TypeError, "hal.component() takes at least 1 argument (0 given)"); + if(n > 2) + raise(PyExc_TypeError, fmt::format("hal.component() takes at most 2 arguments ({} given)", n)); + std::string name = str_arg(args[0], "hal.component", 1); + std::string prefix = n > 1 ? str_arg(args[1], "hal.component", 2) : name; + try { + return std::make_unique(name, prefix); + } catch(const std::system_error &e) { + raise_hal_errno(e.code().value()); + } + })) + .def("setprefix", [](py::handle self, const std::string &prefix) { + live_comp(self).c->setprefix(prefix); + }, "Set the prefix for newly created pins and parameters") + .def("getprefix", [](py::handle self) { + return live_comp(self).c->getprefix(); + }, "Get the prefix for newly created pins and parameters") + .def("newparam", [](py::handle self, const std::string &name, int type, int dir) { + return comp_new_item(self, name, type, dir, false); + }, "Create a new parameter") + .def("newpin", [](py::handle self, const std::string &name, int type, int dir) { + return comp_new_item(self, name, type, dir, true); + }, "Create a new pin") + .def("getitem", [](py::handle self, const std::string &name) { + return comp_get_item(self, name, WHAT_ANY); + }, "Get existing pin or param object") + .def("getpin", [](py::handle self, const std::string &name) { + return comp_get_item(self, name, WHAT_PIN); + }, "Get existing pin object") + .def("getparam", [](py::handle self, const std::string &name) { + return comp_get_item(self, name, WHAT_PARAM); + }, "Get existing param object") + .def("getpins", [](py::handle self, py::args) { + PyComp &pc = live_comp(self); + py::dict d; + for(const auto &[name, p] : pc.c->items()) + d[py::str(name)] = item_read(p); + return d; + }, "Get all pins and values of component") + .def("exit", [](py::handle self) { + // Not a liveness check: exit() of an exited component is fine + PyComp *pc = nullptr; + if(PyObject_TypeCheck(self.ptr(), comp_type)) + pc = py::cast(self); + if(pc) + pc->c.reset(); + }, "Call hal_exit") + .def("ready", [](py::handle self) { + PyComp &pc = live_comp(self); + try { + pc.c->ready(); + } catch(const std::system_error &e) { + raise_hal_errno(e.code().value()); + } + }, "Call hal_ready") + .def("unready", [](py::handle self) { + PyComp &pc = live_comp(self); + try { + pc.c->unready(); + } catch(const std::system_error &e) { + raise_hal_errno(e.code().value()); + } + }, "Call hal_unready") + .def("__repr__", [](py::handle self) { + PyComp *pc = py::cast(self); + if(!pc || !pc->live()) + return fmt::format("", pc ? pc->name : std::string()); + return fmt::format("", + pc->name, pc->c->id(), pc->c->items().size()); + }) + .def("__getattribute__", &comp_getattr) + .def("__setattr__", &comp_setattr) + .def("__delattr__", &comp_delattr) + .def("__getitem__", &comp_getattr) + .def("__setitem__", &comp_setattr) + .def("__delitem__", &comp_delattr) + .def("__len__", [](py::handle self) { return live_comp(self).c->items().size(); }); + + py::class_ item(m, "item", "HAL Pin", py::is_final()); + item.attr("__module__") = "hal"; + item + .def(py::init([](py::args) -> std::unique_ptr { + raise(PyExc_RuntimeError, "Cannot be constructed directly"); + })) + .def("set", [](PyItem &it, py::handle v) { + live_item(it); + item_write(it.pin, v.ptr()); + }, "Set item value") + .def("get", [](const PyItem &it) { + live_item(it); + return item_read(it.pin); + }, "Get item value") + .def("get_type", [](const PyItem &it) { return (int)it.pin.type(); }, "Get item type") + .def("get_dir", [](const PyItem &it) { return (int)it.pin.direction(); }, "Get item direction") + .def("get_name", [](const PyItem &it) { return it.name; }, "Get item name") + .def("is_pin", [](const PyItem &it) { return it.pin.is_pin(); }, "If item is pin or param") + .def("write", &port_write, "Write data to the port queue") + .def("read", [](const PyItem &it, py::handle n) { return port_read_peek(it, n, true); }, + "Read data from the port queue") + .def("writable", [](const PyItem &it) { + const halxx::port &p = check_port(it, "writable"); + check_dir(it, "writable", halxx::dir::OUT); + return p.writable(); + }, "Get the count that can be written to the port") + .def("readable", [](const PyItem &it) { + const halxx::port &p = check_port(it, "readable"); + check_dir(it, "readable", halxx::dir::IN); + return p.readable(); + }, "Get the count that can be read from the port") + .def("peek", [](const PyItem &it, py::handle n) { return port_read_peek(it, n, false); }, + "Read pending data without removal from the queue") + .def("peek_commit", [](const PyItem &it, py::handle n) { + const halxx::port &p = check_port(it, "peek_commit"); + unsigned long l = port_count(it, "peek_commit", n); + if(l < 1 || l > HAL_PORT_SIZE_MAX) + return false; + return p.peek_commit((unsigned)l); + }, "Advance the read pointer by given amount") + .def("clear", [](const PyItem &it) { check_port(it, "clear").clear(); }, "Clear the port queue") + .def("size", [](const PyItem &it) { return check_port(it, "size").size(); }, + "Get the queue size of the port") + .def("__repr__", [](const PyItem &it) { + return fmt::format("", it.pin.is_pin() ? "pin" : "param", it.name, + pin_type2name(it.pin.type()), pin_dir2name(it.pin.direction())); + }); + + py::class_ stream(m, "stream", "HAL Stream", py::is_final()); + stream.attr("__module__") = "hal"; + stream + .def(py::init(&stream_new)) + .def("read", &stream_read) + .def("write", &stream_write) + .def_property_readonly("readable", [](const PyStream &ps) { return ps.s.readable(); }) + .def_property_readonly("writable", [](const PyStream &ps) { return ps.s.writable(); }) + .def_property_readonly("depth", [](const PyStream &ps) { return ps.s.depth(); }) + .def_property_readonly("element_types", [](const PyStream &ps) { return py::bytes(ps.s.typestring()); }) + .def_property_readonly("maxdepth", [](const PyStream &ps) { return ps.s.maxdepth(); }) + .def_property_readonly("num_underruns", [](const PyStream &ps) { return ps.s.num_underruns(); }) + .def_property_readonly("num_overruns", [](const PyStream &ps) { return ps.s.num_overruns(); }) + .def_property_readonly("sampleno", [](const PyStream &ps) { return ps.s.sampleno(); }, + "The number of the last successfully read sample") + .def("__repr__", [](const PyStream &ps) { + return fmt::format("", (unsigned)ps.s.key(), ps.s.is_creator() ? " creator" : ""); + }); + + m.attr("MSG_NONE") = py::int_((int)RTAPI_MSG_NONE); + m.attr("MSG_ERR") = py::int_((int)RTAPI_MSG_ERR); + m.attr("MSG_WARN") = py::int_((int)RTAPI_MSG_WARN); + m.attr("MSG_INFO") = py::int_((int)RTAPI_MSG_INFO); + m.attr("MSG_DBG") = py::int_((int)RTAPI_MSG_DBG); + m.attr("MSG_ALL") = py::int_((int)RTAPI_MSG_ALL); + + m.attr("HAL_BOOL") = py::int_((int)HAL_BOOL); + m.attr("HAL_REAL") = py::int_((int)HAL_REAL); + m.attr("HAL_SINT") = py::int_((int)HAL_SINT); + m.attr("HAL_UINT") = py::int_((int)HAL_UINT); + m.attr("HAL_PORT") = py::int_((int)HAL_PORT); + + m.attr("HAL_RO") = py::int_((int)HAL_RO); + m.attr("HAL_RW") = py::int_((int)HAL_RW); + m.attr("HAL_IN") = py::int_((int)HAL_IN); + m.attr("HAL_OUT") = py::int_((int)HAL_OUT); + m.attr("HAL_IO") = py::int_((int)HAL_IO); + + // IntEnum tagging classes, built from the hal.h constants. Registered + // here so that every consumer shares the same classes. + m.attr("Type") = halenum_build("Type", halenum_type_members); + m.attr("Dir") = halenum_build("Dir", halenum_dir_members); + m.attr("CompType") = halenum_build("CompType", halenum_comp_members); + m.attr("RTType") = halenum_build("RTType", halenum_rt_members); + rttype_enum = py::object(m.attr("RTType")).release(); + + m.attr("REALTIME_TYPE_UNINITIALIZED") = py::int_((int)REALTIME_TYPE_UNINITIALIZED); + m.attr("REALTIME_TYPE_NONE") = py::int_((int)REALTIME_TYPE_NONE); + m.attr("REALTIME_TYPE_UNKNOWN") = py::int_((int)REALTIME_TYPE_UNKNOWN); + m.attr("REALTIME_TYPE_PREEMPT_DYNAMIC") = py::int_((int)REALTIME_TYPE_PREEMPT_DYNAMIC); + m.attr("REALTIME_TYPE_PREEMPT_RT") = py::int_((int)REALTIME_TYPE_PREEMPT_RT); + m.attr("REALTIME_TYPE_RTAI") = py::int_((int)REALTIME_TYPE_RTAI); + m.attr("REALTIME_TYPE_LXRT") = py::int_((int)REALTIME_TYPE_LXRT); + m.attr("REALTIME_TYPE_XENOMAI") = py::int_((int)REALTIME_TYPE_XENOMAI); + m.attr("REALTIME_TYPE_XENOMAI_EVL") = py::int_((int)REALTIME_TYPE_XENOMAI_EVL); + + m.attr("is_kernelspace") = py::int_(rtapi_is_kernelspace()); + m.attr("is_userspace") = py::int_(!rtapi_is_kernelspace()); + + m.attr("streamer_base") = py::int_(0x48535430); + m.attr("sampler_base") = py::int_(0x48534130); #ifdef RTAPI_KERNEL_VERSION - PyModule_AddStringConstant(m, "kernel_version", RTAPI_KERNEL_VERSION); + m.attr("kernel_version") = RTAPI_KERNEL_VERSION; #else - PyModule_AddStringConstant(m, "kernel_version", "Not Available"); + m.attr("kernel_version") = "Not Available"; #endif // Now that we have everything registered, add the halquery sub-module - if(halquery_add_submodule(m) < 0) { - Py_DECREF(m); - return NULL; - } + if(halquery_add_submodule(m.ptr()) < 0) + throw py::error_already_set(); // A halcmd unload arrives as SIGTERM; deliver it to a component as // a KeyboardInterrupt. A program that has already set a handler of @@ -2329,5 +1188,4 @@ PyMODINIT_FUNC PyInit__hal(void) "(lambda s=__import__('signal'):" "s.getsignal(s.SIGTERM) is s.SIG_DFL and " "s.signal(s.SIGTERM, s.default_int_handler))()"); - return m; } diff --git a/tests/hal-cpp/README b/tests/hal-cpp/README new file mode 100644 index 00000000000..a5dbfa57018 --- /dev/null +++ b/tests/hal-cpp/README @@ -0,0 +1,12 @@ +smoke test for the hal Python module (_hal, a pybind11 binding of hal.hh) and +the C++ API in hal.hh itself: component and pin/param lifecycle, item access +and value conversion, 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 across two processes is tests/halmodule/stream. + +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/hal-cpp/cpp_test.cc b/tests/hal-cpp/cpp_test.cc new file mode 100644 index 00000000000..8fd83ff0e0c --- /dev/null +++ b/tests/hal-cpp/cpp_test.cc @@ -0,0 +1,181 @@ +// 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 + +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() +{ + // Keep HAL mapped past the component's exit, for the by-name check at + // the end. + if(hal_lib_init()) { + printf("FAIL - hal_lib_init\n"); + return 1; + } + 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"); + + // By-name queries + { + auto q = hal::pin_param_query("halcpp-test.in"); + CHECK(q && q->qtype == HAL_QTYPE_PIN && q->pp.type == HAL_SINT && hal_get_sint(q->pp.ref.s) == 42 + && q->pp.signal && std::string(q->pp.signal) == "halcpp-sig", "pin_param_query pin"); + q = hal::pin_param_query("halcpp-test.gain", HAL_QTYPE_PIN); + CHECK(!q, "pin_param_query limited to pins skips a param"); + q = hal::pin_param_query("halcpp-test.gain"); + CHECK(q && q->qtype == HAL_QTYPE_PARAM && hal_get_real(q->pp.ref.r) == 3.0, "pin_param_query param"); + auto s = hal::signal_query("halcpp-sig"); + CHECK(s && s->sig.type == HAL_SINT && hal_get_sint(s->sig.ref.s) == 42 && s->sig.readers == 1, + "signal_query"); + CHECK(!hal::signal_query("no-such-thing") && !hal::pin_param_query("no-such-thing"), + "queries of a missing name are empty"); + } + + // Error paths + bool threw = false; + try { hal::get_value("no-such-thing"); } catch(const std::system_error &e) { threw = ENOENT == e.code().value(); } + CHECK(threw, "get_value missing name throws"); + threw = false; + try { hal::set_value("halcpp-test.mode", 5); } + catch(const std::system_error &e) { threw = e.code().value() == EACCES; } + CHECK(threw, "set_value on RO param throws EACCES"); + 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 < 4; i++) { + ok = ok && s.writable(); + s.write({(rtapi_bool)(i % 2), (rtapi_real)i, (rtapi_sint)i, (rtapi_uint)i}); + } + CHECK(ok, "4 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 < 4; 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, "4 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++; + } + + hal_lib_exit(); + printf(failures ? "%d FAILURES\n" : "ALL C++ TESTS PASSED\n", failures); + return failures ? 1 : 0; +} diff --git a/tests/hal-cpp/expected b/tests/hal-cpp/expected new file mode 100644 index 00000000000..3b560eee338 --- /dev/null +++ b/tests/hal-cpp/expected @@ -0,0 +1,202 @@ +ok - hal.component subclasses _hal.component +ok - component class name +ok - item and stream class names +ok - hal.error +ok - hal.shm is gone +ok - Type enum +ok - enum aliases and canonical members +ok - CompType and RTType +ok - int constants +ok - stream key bases +ok - get_realtime_type returns an RTType +ok - is_initialized +ok - is_userspace and kernel_version +ok - _hal has no deprecated names +ok - component repr +ok - len() of an empty component is 0 +ok - default prefix is the name +ok - component needs a name +ok - duplicate component name is hal.error +ok - newpin returns a hal.Pin +ok - item name is the short name +ok - get_type is a plain int +ok - get_dir is a plain int +ok - is_pin +ok - item repr +ok - duplicate pin name +ok - invalid pin type +ok - port param refused +ok - pin name too long +ok - no keyword arguments +ok - unbound _hal.component.newpin call +ok - item cannot be constructed +ok - newparam returns a hal.Param +ok - param repr +ok - param name shares the pin namespace +ok - param set/get +ok - int into a real pin +ok - text into a real pin +ok - bad text into a real pin +ok - None into a real pin +ok - huge int into a real pin +ok - 64-bit sint +ok - float truncates into a sint +ok - text into a sint +ok - sint range +ok - 64-bit uint +ok - negative into a uint raises, value kept +ok - bool param from True +ok - bool param from 0.0 +ok - bool param from -0.5 +ok - bool param from 2 +ok - bool param from 0 +ok - bool param from 'yes' +ok - bool param from 'OFF' +ok - bool param from '1' +ok - bool param from '2' +ok - bad text into a bool +ok - values are plain bool/int/float +ok - hal.Pin value property +ok - attribute access reads the pin +ok - attribute assignment writes the param +ok - normal lookup first, also for h[name] +ok - assignment to an unknown name +ok - lookup of an unknown name +ok - del h[pin] raises +ok - del h.pin raises +ok - del of an unknown name +ok - len() counts pins and params +ok - getpins covers pins and params +ok - getpin of a param +ok - getparam of a pin +ok - getitem missing +ok - setprefix affects new pins +ok - hal.Pin has the port calls only for a port +ok - unlinked port has no buffer +ok - write to an unlinked port fails +ok - read from an unlinked port fails +ok - assignment to a port pin +ok - set() of a port pin +ok - component ready +ok - component queries of a missing name +ok - new_sig returns False on success +ok - new_sig returns True on failure +ok - new_sig type +ok - connect returns False on success +ok - disconnect returns True on failure +ok - pin_has_writer: no writer +ok - pin_has_writer of a missing pin +ok - set_s returns True +ok - get_s/get_value of a signal +ok - connected pin reads the signal +ok - set_p from text +ok - set_p float into bool +ok - set_p/get_value beyond 32 bits +ok - set_p conversion failure +ok - set_p of an output pin +ok - set_p of a missing pin +ok - set_s of a missing signal +ok - get_p/get_s of a missing name +ok - get_value of a missing name +ok - get_info_pins +ok - get_info_params +ok - get_info_signals +ok - port new_sig +ok - link port writer +ok - link port reader +ok - port signal reads as its buffer size +ok - port value is the buffer size +ok - get_info_signals of a port signal +ok - get_info_pins of a port pin +ok - port write bytes +ok - port write str as UTF-8 +ok - readable/writable +ok - writable of a reader +ok - read of a writer +ok - read count type +ok - write data type +ok - port call on a scalar pin +ok - peek +ok - peek_commit +ok - read +ok - read of an empty port +ok - write beyond the buffer fails +ok - clear +ok - invalid typestring +ok - bad stream key +ok - stream needs a component +ok - stream repr +ok - element_types and maxdepth +ok - stream full +ok - write to a full stream +ok - too few elements +ok - too many elements +ok - write takes a tuple +ok - sampleno is read-only +ok - 10 samples read back in order +ok - read of an empty stream +ok - an item keeps its component +ok - the item still works +ok - the component goes with its last item +ok - exit removes component +ok - repr of an exited component +ok - attribute lookup after exit +ok - len() after exit +ok - item access after exit +ok - item get() after exit +ok - unbound call after exit +ok - exit() twice + +ALL TESTS PASSED +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 - pin_param_query pin +ok - pin_param_query limited to pins skips a param +ok - pin_param_query param +ok - signal_query +ok - queries of a missing name are empty +ok - get_value missing name throws +ok - set_value on RO param throws EACCES +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 - 4 samples written +ok - stream full +ok - wrong element count throws +ok - out-of-range element throws +ok - 4 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/hal-cpp/skip b/tests/hal-cpp/skip new file mode 100755 index 00000000000..088ae2947dc --- /dev/null +++ b/tests/hal-cpp/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/hal-cpp/smoke.hal b/tests/hal-cpp/smoke.hal new file mode 100644 index 00000000000..46d0b96d081 --- /dev/null +++ b/tests/hal-cpp/smoke.hal @@ -0,0 +1 @@ +loadusr -w ./smoke.py diff --git a/tests/hal-cpp/smoke.py b/tests/hal-cpp/smoke.py new file mode 100755 index 00000000000..b796c0195f6 --- /dev/null +++ b/tests/hal-cpp/smoke.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +# Smoke test for the hal Python module (_hal on hal.hh). Run inside a live +# halrun environment. +import gc +import sys + +import _hal +import hal + +failures = [] + +def check(cond, msg): + if cond: + print("ok -", msg) + else: + print("FAIL -", msg) + failures.append(msg) + +def raises(exc, fn, *a): + try: + fn(*a) + except exc as e: + return str(e) + except Exception as e: + return "wrong exception %s: %s" % (type(e).__name__, e) + return None + +# --- module surface --------------------------------------------------------- +check(hal.component.__mro__[1] is _hal.component, "hal.component subclasses _hal.component") +check(_hal.component.__module__ == "hal" and _hal.component.__name__ == "component", "component class name") +check(_hal.item.__module__ == "hal" and _hal.stream.__module__ == "hal", "item and stream class names") +check(hal.error.__module__ == "hal" and issubclass(hal.error, Exception), "hal.error") +check(not hasattr(_hal, "shm"), "hal.shm is gone") +check(hal.Type.__module__ == "hal" and repr(hal.Type.BOOL) == "", "Type enum") +check(hal.Type.HAL_PORT is hal.Type.PORT and list(hal.Dir) == [hal.Dir.IN, hal.Dir.OUT, hal.Dir.IO, hal.Dir.RO, hal.Dir.RW], + "enum aliases and canonical members") +check(hal.CompType.UNKNOWN == -1 and hal.RTType.XENOMAI_EVL == 7, "CompType and RTType") +check(type(hal.HAL_PORT) is int and hal.HAL_RW == 192 and hal.MSG_ALL == 5, "int constants") +check(hal.streamer_base == 0x48535430 and hal.sampler_base == 0x48534130, "stream key bases") +check(isinstance(hal.get_realtime_type(), hal.RTType), "get_realtime_type returns an RTType") +check(hal.is_initialized() is True, "is_initialized") +check(type(hal.is_userspace) is int and type(hal.kernel_version) is str, "is_userspace and kernel_version") +check(raises(AttributeError, getattr, _hal, "HAL_BIT") is not None, "_hal has no deprecated names") + +# --- component lifecycle ------------------------------------------------- +h = hal.component("halcpp-test") +check(repr(h) == "" % hal.query.comp("halcpp-test")["id"], + "component repr") +check(len(h) == 0 and not h, "len() of an empty component is 0") +check(h.getprefix() == "halcpp-test", "default prefix is the name") +check(raises(TypeError, hal.component) is not None, "component needs a name") +check(raises(hal.error, hal.component, "halcpp-test") is not None, "duplicate component name is hal.error") + +# --- pins and params -------------------------------------------------------- +p_bool = h.newpin("bool-out", hal.Type.BOOL, hal.Dir.OUT) +p_real = h.newpin("real-in", hal.Type.REAL, hal.Dir.IN) +p_sint = h.newpin("sint-io", hal.Type.SINT, hal.Dir.IO) +p_uint = h.newpin("uint-io", int(hal.Type.UINT), int(hal.Dir.IO)) +check(isinstance(p_bool, hal.Pin) and isinstance(p_bool._item, _hal.item), "newpin returns a hal.Pin") +check(p_bool.get_name() == "bool-out" and p_bool.name == "bool-out", "item name is the short name") +check(type(p_bool.get_type()) is int and p_bool.get_type() == hal.Type.BOOL, "get_type is a plain int") +check(type(p_bool.get_dir()) is int and p_bool.get_dir() == hal.Dir.OUT, "get_dir is a plain int") +check(p_bool.is_pin() is True, "is_pin") +check(repr(p_sint._item) == '', "item repr") +check(raises(ValueError, h.newpin, "bool-out", hal.Type.BOOL, hal.Dir.OUT) == "Duplicate pin name 'bool-out'", + "duplicate pin name") +check(raises(hal.error, h.newpin, "bad", 99, hal.Dir.IN) == "Invalid pin type 99", "invalid pin type") +check(raises(hal.error, h.newparam, "port-param", hal.Type.PORT, hal.Dir.RW) == "Invalid param type 5", + "port param refused") +check(raises(hal.error, h.newpin, "x" * 60, hal.Type.BOOL, hal.Dir.IN) + == 'Invalid pin name length "halcpp-test.%s": max = 55 characters' % ("x" * 60), "pin name too long") +check(raises(TypeError, lambda: h.newpin("kw", hal.Type.BOOL, dir=hal.Dir.IN)) is not None, "no keyword arguments") +p_raw = _hal.component.newpin(h, "unbound", hal.Type.SINT, hal.Dir.OUT) +check(type(p_raw) is _hal.item, "unbound _hal.component.newpin call") +check(raises(RuntimeError, _hal.item) == "Cannot be constructed directly", "item cannot be constructed") + +pm = h.newparam("gain", hal.Type.REAL, hal.Dir.RW) +pb = h.newparam("enable", hal.Type.BOOL, hal.Dir.RW) +check(isinstance(pm, hal.Param) and pm.is_pin() is False, "newparam returns a hal.Param") +check(repr(pm._item) == '', "param repr") +check(raises(ValueError, h.newparam, "real-in", hal.Type.REAL, hal.Dir.RW) == "Duplicate parameter name 'real-in'", + "param name shares the pin namespace") +pm.set(2.5) +check(pm.get() == 2.5 and h["gain"] == 2.5, "param set/get") + +# --- item access and conversion --------------------------------------------- +h["real-in"] = 3 +check(type(h["real-in"]) is float and h["real-in"] == 3.0, "int into a real pin") +h["real-in"] = "1.25" +check(h["real-in"] == 1.25, "text into a real pin") +check(raises(TypeError, h.__setitem__, "real-in", "abc") == "Number expected, not str", "bad text into a real pin") +check(raises(TypeError, h.__setitem__, "real-in", None) == "Number expected, not NoneType", "None into a real pin") +check(raises(OverflowError, h.__setitem__, "real-in", 1 << 1024) is not None, "huge int into a real pin") +h["sint-io"] = -2**62 +check(h["sint-io"] == -2**62, "64-bit sint") +h["sint-io"] = 2.9 +check(h["sint-io"] == 2, "float truncates into a sint") +h["sint-io"] = "-7" +check(h["sint-io"] == -7, "text into a sint") +check(raises(OverflowError, h.__setitem__, "sint-io", 2**63) is not None, "sint range") +h["uint-io"] = 2**64 - 1 +check(h["uint-io"] == 2**64 - 1, "64-bit uint") +check(raises(OverflowError, h.__setitem__, "uint-io", -1) is not None and h["uint-io"] == 2**64 - 1, + "negative into a uint raises, value kept") +for v, want in ((True, True), (0.0, False), (-0.5, True), (2, True), (0, False), ("yes", True), + ("OFF", False), ("1", True), ("2", True)): + h["enable"] = v + check(h["enable"] is want, "bool param from %r" % (v,)) +check(raises(ValueError, h.__setitem__, "enable", "maybe") is not None, "bad text into a bool") +check(type(h["bool-out"]) is bool and type(h["sint-io"]) is int, "values are plain bool/int/float") + +p_sint.value = 11 +check(h.getitem("sint-io").get() == 11 and p_sint.value == 11, "hal.Pin value property") +check(h["sint-io"] == getattr(h, "sint-io") == 11, "attribute access reads the pin") +h.gain = 4.5 +check(pm.get() == 4.5, "attribute assignment writes the param") +check(callable(h["newpin"]) and h["getprefix"]() == "halcpp-test", "normal lookup first, also for h[name]") +check(raises(AttributeError, setattr, h, "nonesuch", 1) == "Pin or param 'nonesuch' does not exist", + "assignment to an unknown name") +check(raises(AttributeError, getattr, h, "nonesuch") == "Pin or param 'nonesuch' does not exist", + "lookup of an unknown name") +check(raises(TypeError, h.__delitem__, "gain") is not None, "del h[pin] raises") +check(raises(TypeError, delattr, h, "gain") is not None, "del h.pin raises") +check(raises(AttributeError, delattr, h, "nonesuch") is not None, "del of an unknown name") +check(len(h) == 7 and bool(h), "len() counts pins and params") +pins = h.getpins() +check(list(pins) == ["bool-out", "enable", "gain", "real-in", "sint-io", "uint-io", "unbound"] + and pins["gain"] == 4.5, "getpins covers pins and params") +check(raises(AttributeError, h.getpin, "gain") == "Pin 'gain' does not exist", "getpin of a param") +check(raises(AttributeError, h.getparam, "real-in") == "Param 'real-in' does not exist", "getparam of a pin") +check(raises(AttributeError, h.getitem, "nonesuch") == "Pin or param 'nonesuch' does not exist", "getitem missing") + +# --- prefix ------------------------------------------------------------------- +h.setprefix("halcpp-renamed") +p2 = h.newpin("later", hal.Type.UINT, hal.Dir.OUT) +check(p2.get_name() == "later" and hal.query.pin("halcpp-renamed.later") is not None, + "setprefix affects new pins") +h.setprefix("halcpp-test") + +# --- ports -------------------------------------------------------------------- +p_pout = h.newpin("port-out", hal.Type.PORT, hal.Dir.OUT) +p_pin = h.newpin("port-in", hal.Type.PORT, hal.Dir.IN) +check(p_pout.get_type() == hal.Type.PORT and hasattr(p_pout, "write") and not hasattr(p_bool, "write"), + "hal.Pin has the port calls only for a port") +check(h["port-out"] == 0 and p_pin._item.readable() == 0, "unlinked port has no buffer") +check(p_pout.write(b"x") is False, "write to an unlinked port fails") +check(p_pin.read(1) is False, "read from an unlinked port fails") +check(raises(hal.error, h.__setitem__, "port-in", 1) == "Invalid pin type 5", "assignment to a port pin") +check(raises(hal.error, p_pin.set, 1) == "Invalid pin type 5", "set() of a port pin") + +h.ready() +check(hal.component_exists("halcpp-test") and hal.component_is_ready("halcpp-test"), "component ready") +check(hal.component_exists("no-such-comp") is False and hal.component_is_ready("no-such-comp") is False, + "component queries of a missing name") + +# --- signals and by-name access ----------------------------------------------- +check(hal.new_sig("halcpp-sig", hal.Type.REAL) is False, "new_sig returns False on success") +check(hal.new_sig("halcpp-sig", hal.Type.REAL) is True, "new_sig returns True on failure") +check(raises(RuntimeError, hal.new_sig, "halcpp-bad", 9) == "not a valid HAL signal type", "new_sig type") +check(hal.connect("halcpp-test.real-in", "halcpp-sig") is False, "connect returns False on success") +check(hal.disconnect("no-such-pin") is True, "disconnect returns True on failure") +check(hal.pin_has_writer("halcpp-test.real-in") is False, "pin_has_writer: no writer") +check(raises(NameError, hal.pin_has_writer, "no-such-pin") == "Pin `no-such-pin' does not exist", + "pin_has_writer of a missing pin") +check(hal.set_s("halcpp-sig", 7.5) is True, "set_s returns True") +check(hal.get_s("halcpp-sig") == 7.5 and hal.get_value("halcpp-sig") == 7.5, "get_s/get_value of a signal") +check(hal.get_p("halcpp-test.real-in") == 7.5 and h["real-in"] == 7.5, "connected pin reads the signal") +check(hal.set_p("halcpp-test.gain", "6.5") is True and pm.get() == 6.5, "set_p from text") +check(hal.set_p("halcpp-test.enable", 0.0) is True and h["enable"] is False, "set_p float into bool") +check(hal.set_p("halcpp-test.sint-io", 2**40) is True and hal.get_value("halcpp-test.sint-io") == 2**40, + "set_p/get_value beyond 32 bits") +check(raises(RuntimeError, hal.set_p, "halcpp-test.uint-io", -1) == "set_p: halcpp-test.uint-io: invalid argument", + "set_p conversion failure") +check(raises(RuntimeError, hal.set_p, "halcpp-test.bool-out", 1) == "set_p: halcpp-test.bool-out: not writable", + "set_p of an output pin") +check(raises(RuntimeError, hal.set_p, "no-such-pin", 1) == "set_p: no-such-pin: not found", "set_p of a missing pin") +check(raises(RuntimeError, hal.set_s, "no-such-sig", 1) == "set_s: no-such-sig: not found", "set_s of a missing signal") +check(hal.get_p("no-such-pin") is None and hal.get_s("no-such-sig") is None, "get_p/get_s of a missing name") +check(raises(RuntimeError, hal.get_value, "no-such-pin") == "get_value: no-such-pin: not found", + "get_value of a missing name") +info = {d["NAME"]: d for d in hal.get_info_pins()} +d = info["halcpp-test.real-in"] +check(list(d) == ["NAME", "VALUE", "DIRECTION", "TYPE"] and d["VALUE"] == 7.5 + and d["DIRECTION"] == hal.Dir.IN and type(d["TYPE"]) is int, "get_info_pins") +info = {d["NAME"]: d for d in hal.get_info_params()} +check(info["halcpp-test.gain"]["VALUE"] == 6.5, "get_info_params") +info = {d["NAME"]: d for d in hal.get_info_signals()} +d = info["halcpp-sig"] +check(list(d) == ["NAME", "VALUE", "DRIVER", "TYPE"] and d["VALUE"] == 7.5 and d["DRIVER"] is None, + "get_info_signals") + +# --- ports, linked -------------------------------------------------------------- +check(hal.new_sig("halcpp-port", hal.Type.PORT) is False, "port new_sig") +check(hal.connect("halcpp-test.port-out", "halcpp-port") is False, "link port writer") +check(hal.connect("halcpp-test.port-in", "halcpp-port") is False, "link port reader") +hal.set_s("halcpp-port", "16") +check(hal.get_s("halcpp-port") == 16, "port signal reads as its buffer size") +check(h["port-in"] == 16 and p_pin.size() == 16 and p_pout.size() == 16, "port value is the buffer size") +info = {d["NAME"]: d for d in hal.get_info_signals()} +check(info["halcpp-port"]["VALUE"] == 16 and info["halcpp-port"]["DRIVER"] == "halcpp-test.port-out", + "get_info_signals of a port signal") +info = {d["NAME"]: d for d in hal.get_info_pins()} +check(info["halcpp-test.port-in"]["VALUE"] == 16, "get_info_pins of a port pin") +check(p_pout.write(b"hello") is True, "port write bytes") +check(p_pout.write("wörld") is True, "port write str as UTF-8") +check(p_pin._item.readable() == 11 and p_pout._item.writable() == 16 - 1 - 11, "readable/writable") +check(raises(RuntimeError, p_pin._item.writable) == "writable: port-in: Pin not output", "writable of a reader") +check(raises(RuntimeError, p_pout.read, 1) == "read: port-out: Pin not input", "read of a writer") +check(raises(RuntimeError, p_pin.read, "1") == "read: port-in: Argument not an integer", "read count type") +check(raises(RuntimeError, p_pout.write, 1) == "write: port-out: Argument not a string or bytes object", + "write data type") +check(raises(RuntimeError, p_sint._item.size) == "size: sint-io: Pin type not HAL_PORT but '3'", + "port call on a scalar pin") +check(p_pin.peek(5) == b"hello" and p_pin._item.readable() == 11, "peek") +check(p_pin.peek_commit(5) is True, "peek_commit") +check(p_pin.read(6) == "wörld".encode(), "read") +check(p_pin.read(1) is False, "read of an empty port") +check(p_pout.write(b"x" * 16) is False and p_pin._item.readable() == 0, "write beyond the buffer fails") +p_pout.write(b"abc") +p_pin.clear() +check(p_pin._item.readable() == 0, "clear") + +# --- streams -------------------------------------------------------------------- +check(raises(OSError, hal.stream, h, hal.streamer_base, 10, "xx") is not None, "invalid typestring") +check(raises(TypeError, hal.stream, h, "key") is not None, "bad stream key") +check(raises(TypeError, hal.stream, 1, hal.streamer_base) is not None, "stream needs a component") +s = hal.stream(h, hal.streamer_base, 10, "bfsu") +check(repr(s) == "" % hal.streamer_base, "stream repr") +check(s.element_types == b"bfsu" and type(s.maxdepth) is int and s.maxdepth == 10, "element_types and maxdepth") +for i in range(10): + s.write((i % 2, i, i, i)) +check(not s.writable and s.depth == 10, "stream full") +check(raises(OSError, s.write, (1, 1, 1, 1)) is not None and s.num_overruns == 1, "write to a full stream") +check(raises(ValueError, s.write, (1, 1, 1)) == "Too few elements to unpack", "too few elements") +check(raises(ValueError, s.write, (1, 1, 1, 1, 1)) == "Too many elements to unpack", "too many elements") +check(raises(TypeError, s.write, [1, 1, 1, 1]) is not None, "write takes a tuple") +check(raises(AttributeError, setattr, s, "sampleno", 3) is not None, "sampleno is read-only") +ok = True +for i in range(10): + ok = ok and s.read() == (bool(i % 2), float(i), i, i) and s.sampleno == i + 1 +check(ok, "10 samples read back in order") +check(s.read() is None and s.num_underruns == 1, "read of an empty stream") +del s + +# --- lifetime --------------------------------------------------------------------- +def make_pin(): + c = hal.component("halcpp-scoped") + return c.newpin("p", hal.Type.SINT, hal.Dir.OUT) +p_scoped = make_pin() +gc.collect() +check(hal.component_exists("halcpp-scoped"), "an item keeps its component") +p_scoped.set(5) +check(p_scoped.get() == 5, "the item still works") +del p_scoped +gc.collect() +check(not hal.component_exists("halcpp-scoped"), "the component goes with its last item") + +h.exit() +check(not hal.component_exists("halcpp-test"), "exit removes component") +check(repr(h) == "", "repr of an exited component") +closed = "Invalid operation on closed HAL component" +check(raises(RuntimeError, getattr, h, "newpin") == closed, "attribute lookup after exit") +check(raises(RuntimeError, len, h) == closed, "len() after exit") +check(raises(RuntimeError, lambda: h["gain"]) == closed, "item access after exit") +check(raises(RuntimeError, p_sint.get) == closed, "item get() after exit") +check(raises(RuntimeError, _hal.component.newpin, h, "x", hal.Type.BOOL, hal.Dir.IN) == closed, + "unbound call after exit") +_hal.component.exit(h) +check(True, "exit() twice") + +print() +if failures: + print(f"{len(failures)} FAILURES") + sys.exit(1) +print("ALL TESTS PASSED") diff --git a/tests/hal-cpp/test.sh b/tests/hal-cpp/test.sh new file mode 100755 index 00000000000..220e703e1a9 --- /dev/null +++ b/tests/hal-cpp/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 <" % hal.streamer_base +assert reader.element_types == b"bfsu" +assert reader.maxdepth == 10 for i in range(10): assert reader.readable - assert reader.read() == ((i % 2, i, i, i)) + sample = reader.read() + assert sample == ((i % 2, i, i, i)) + assert [type(v) for v in sample] == [bool, float, int, int] 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: + hal.stream(c, hal.streamer_base, "bfsf") +except OSError: + pass +else: + assert False, "attach with a mismatched typestring should fail" # report before ready(): halrun ends the run once it sees ready print("pass", flush=True) c.ready() diff --git a/tests/halmodule/stream/stream_writer.py b/tests/halmodule/stream/stream_writer.py index edc78c5a206..1fae5fb6cfa 100755 --- a/tests/halmodule/stream/stream_writer.py +++ b/tests/halmodule/stream/stream_writer.py @@ -3,6 +3,7 @@ import time c = hal.component("stream_writer") writer = hal.stream(c, hal.streamer_base, 10, "bfsu") +assert repr(writer) == "" % hal.streamer_base for i in range(10): assert writer.writable