hal: C++ API (hal.hh) and pybind11 bindings on the new HAL API - #4251
grandixximo wants to merge 1 commit into
Conversation
|
This seems to be working, but I am not sure if it is the right shape we want, these are the direction I took on the issue raised in the meeting, subject to revision.
@BsAtHome A bare hal_sint_t is width-blind, and wrapping foreign raw handles across widths is unsafe in the interim. But the C++ layer never selects from the handle alone. Component-created pins carry the type at construction via traits (int32_t vs int64_t are distinct C++ types, compile-time selection), and the runtime path uses q->pp.type from your query API to pick the variant alternative, the stored-type-tag multiplex you described in #4099. The only unsafe path is explicitly reinterpreting a raw handle, which can be guarded with a query lookup. So the layer works transitionally, it just gets simpler after the break. |
|
oh, that was quick. I think this should be separate from the other big PR, as this can go in master fairly quickly. the only reason I stopped was that buster had no pybind11 package. this should also replace the current pyhal/halmodule instead of adding a third module. and, the same cpp api should be used in xhc, task and other places which are implemented in cpp. |
|
Buster is basically not supported anymore with master, I will fix the review points tomorrow, thank you both |
a8c2a71 to
87ff92b
Compare
|
There is another issue. The new hal_lib uses a reference counted init/exit and has separated out Calling The python class must initialize using hal_lib_init() and terminate by calling hal_lib_exit(). If the user has not called hal_exit() on any created components, you will now get a proper error message (because the user forgot to terminate correctly). |
|
can you rename the module to halmodule, so that the UIs and the tests that are already in place use the new bindings, and remove the old ones? |
Replacing halmodule is the goal, agreed, but renaming now would break things for two separate reasons. First, a hard technical one: on master the new bindings cannot cover the existing Python surface. Second, the compatibility tail: So the proposal stands as: land this additive now (hal.hh + halpp alongside, nothing replaced), and do the rename + replacement as a follow-up once #4247 is in. Your old branch's consumer sweep (hal_glib, qtvcp core, raster, tests) is the starting checklist for that PR. Is this acceptable? Open for discussion... |
|
I see that #4231 changes halmodule.cc, we should also add those functions via c++/pybind11 |
No, all of this should be in halquery. |
What is halquery? |
That is the new interface to all of HAL's internals nicely wrapped up from the hal query interface without someone needing to dig into HAL's internals. Hal_lib's internals are fully isolated in #4247 and nobody should be writing code that circumvents that. Inclusion of hal_priv.h is strictly forbidden in any code that is not part of hal_lib. |
|
ah, found it. I would much prefer if it would be a c++ api, which can be used to automatically generate the python bindings. |
|
It is a pure C version, so it does not have the same problems as C++ interfacing C-based Python ;-) |
|
This is exactly the split #4251 (this PR) implements: the query API stays pure C in the library (Bertho's layer, no bypass possible), and the C++ API sits on top of it (hal.hh), which is where Python bindings get generated (halpybind) , no hand-written CPython code. halquery.c proves the query API is sufficient; the same functions can be exposed as pybind wrappers over |
|
Fine by me to get it C++ wrapped. It does not yet have priority one. There is one thing that should be fixed in both halmodule and halquery: use IntEnum types for both type and direction. It may be necessary to have a |
|
A shared pure-Python |
That would be the simple way. If it works, then it should be nice. The real benefit is in halquery to have the iteration result show the right name instead of a number. There it is big value. Probably also need to add the same for other constants.
Global namespace naming and fixing hierarchy is a problem we need to fix in a later version. This would be a version 3.0 thing, I guess ;-)
Ha, and so much for "C++ is easy to maintain" ;-) |
202386a to
6925b79
Compare
|
Going back to draft, waiting for master new API to materialize ;-) |
33012db to
63c8a05
Compare
|
Branch rewritten and pushed (33012db => 63c8a05), four commits on current master:
Build notes: the pybind11 bindings are skipped with a note when the headers are absent, not failed, so the tree still builds without python3-pybind11; tests/halpp self-skips in that case. hal.hh is exported to include/ alongside hal.h. tests/halpp/test.sh compiles cpp_test.cc against the tree and runs it in a live HAL session, so the native C++ side has CI coverage too. Still draft on purpose: #4247 moves the same ground (hal.hh, the type system, HAL isolation), so this waits for that to land before the query bindings follow as hal.query. |
63c8a05 to
7beaaf3
Compare
|
Branch reworked after feedback Bertho sent me in a private mail (63c8a05 => 7beaaf3). The haltype.py commit is gone, replaced by:
pybind11 note: real enum.IntEnum via py::native_enum needs pybind11 >= 3.0 and trixie ships 2.13, so construction goes through the enum module's functional API. The classes are genuine IntEnums either way, so moving to py::native_enum later (TODO in the header) is invisible to user code. Query results stay plain dicts as discussed; the future hal.query bindings tag type/dir with these classes, no per-function wrappers. tests/haltype.0 is now a sanity suite for the classes, tests/halpp checks halpp shares them. All HAL suites pass. |
7beaaf3 to
d24bbd7
Compare
d24bbd7 to
e9fd2bf
Compare
e9fd2bf to
9de1f11
Compare
9de1f11 to
8980d3e
Compare
|
@BsAtHome one API question from adding port pins: hal_pin_new(3) says hal_get_p(3) cannot be called on a HAL_PORT pin, but the library returns success with value 0 for it, and hal.query reports the pin's buffer size. hal.hh's get_value() currently relies on hal_get_p() succeeding for port pins (a callback fills in the buffer size). Which behavior is intended? If get_p on a port pin should fail, I'll switch get_value() to another route and the man page stays as is; otherwise the man page needs a line. |
|
Tricky question... The port pins are only end-point nodes. They do not have any real meaning besides read/write. The size of the buffer should ideally be retrieved using hal_get_s because the port pins can never stand alone and need a binding signal. The hal_set_s on that signal sets the buffer size, which makes it logical to retrieve the size using hal_get_s. The fact that the hal_get_p on the pins also return the size is due to legacy IIRC. There is no "harm" in that you can call hal_get_p on a port pin. However, the question in the room is whether hal_get_p on an output pin really should be retrieving one byte of the queue and hal_set_p on the input pin writes one byte into the queue. The queue's size is managed by the signal. |
|
Thanks, that settles it. I'll keep the pin side out of it: in hal.hh a port pin gets no scalar value (get_value() on a port pin and the runtime-typed get() raise), the buffer size comes from the signal via hal_get_s(), and port::size() stays as the explicit call on the handle. That leaves hal_get_p()/hal_set_p() on port pins free for whatever you decide, including byte I/O. Small correction for the record: the library's hal_get_p() on a port pin returns 0, not the size (get_common() is called with getport=0 for pins); only hal.query computes the size for pins itself. |
Reintroduce a C++ interface for HAL, replacing the old hal.hh that was removed ahead of the API break. It is built strictly on the public C API and the query API: no hal_priv.h, no direct shared memory access, no re-implemented library internals. - hal.hh: type-safe, header-only C++ layer in linuxcnc::hal, exported to include/. Typed pin/param handles via traits<T> over rtapi_bool, rtapi_sint, rtapi_uint and rtapi_real (compile-time accessor selection, no 32-bit handles), a runtime-typed pin_t variant and anypin for name-based access, a component class with add_pin for the struct-member idiom, and ULAPI by-name query/set functions on hal_get_p/hal_set_p/hal_get_s/hal_set_s/hal_comp_by_name. Handles re-read the shmem slot on every access so hal_link() slot rewrites stay visible (C pointer-variable semantics). Handles are move-only; HAL objects are unique. Query callback paths are exception-free; range errors are reported after the library releases the HAL mutex. - Streams: linuxcnc::hal::stream, a move-only wrapper around hal_stream_t, created with a depth and a typestring or attached to an existing key. Element types drive the conversion of samples in both directions with the same range checks as the by-name setters. Library failures reported as a negative errno are thrown as std::system_error. - halpybind.cc: pybind11 module (halpp.so) exposing component, Pin, stream and the by-name functions. Built alongside _hal/hal.py, replacing nothing. Type and direction tags are the _hal.Type/_hal.Dir IntEnum classes: arguments accept the members or plain ints, results come back as members. Importing _hal also initializes the HAL library. String set values go through setps_common_cb for halcmd-consistent parsing, std::system_error becomes OSError, and SIGTERM raises KeyboardInterrupt as it does in _hal. - Ports: linuxcnc::hal::port, a move-only handle for HAL_PORT pins with all-or-nothing read/peek/peek_commit/write plus readable/writable/size/clear, created with component::newport() or runtime-typed newpin(). The buffer belongs to the linking signal and is sized with set_signal(); an unlinked port has no buffer and its reads and writes fail quietly. A port pin has no scalar value: get_value() and the runtime-typed get/set raise for it, leaving hal_get_p()/hal_set_p() semantics on port pins to the library. The size is read from the signal (get_value() on a port signal) or with port::size(). halpp exposes the same calls on Pin, with bytes in and out (str is written as UTF-8). - tests/halpp: Python and native C++ smoke suites, plus a stream create/attach pair across two processes the way sampler and streamer are used. The C++ suite compiles against the tree, so the test is skipped for installed packages. Based on the pybind11 branch by rene-dev, rebuilt on the new HAL API.
8980d3e to
e5e5920
Compare
@rene-dev @BsAtHome - this is the unification pass we discussed in the #4099 meeting: Rene's C++/pybind11 interface rebuilt on Bertho's getter/setter API, satisfying both constraints (no HAL internals access; simple, low-maintenance, self-documenting C++ surface).
Rebased onto master after #4565, as a single commit. It fills the slot left by the removal of the old
hal.hhand is otherwise additive:_hal/hal.pyare untouched and nothing is replaced. The two earlier commits forhal.queryregistration and theType/DirIntEnums are gone, since master now carries both.What this adds
src/hal/hal.hh- type-safe C++ layer, header-only, innamespace linuxcnc::hal, exported toinclude/:hal::pin<T>typed handles viatraits<T>overrtapi_bool,rtapi_sint,rtapi_uintandrtapi_real. 64-bit only, matching the API break: there are no 32-bit handles. Handles re-read the shmem slot per access, sohal_link()rewrites stay visible (same semantics as C pin pointers). Handles are move-only; HAL objects are unique.hal::pin_tvariant +hal::anypin: runtime-typed access multiplexes on the variant tag (the stored-type pattern required for name-keyed collections).hal::component: pins + params,newpin/newparam(typed and runtime-typed),add_pinfor the struct-member idiom, item access, prefix handling.hal::port: move-only handle forHAL_PORTpins, created withnewport()or the runtime-typednewpin(). All-or-nothingread/peek/peek_commit/writeplusreadable/writable/size/clear. The buffer belongs to the linking signal and is sized withset_signal()(sets); an unlinked port has no buffer, so its reads and writes fail without log noise. By name and through the runtime-typed item a port reads as its buffer size, likehal.query, and cannot be set.hal::stream: move-only wrapper aroundhal_stream_t, create or attach, with the same conversion and range checks as the by-name setters.get_value,set_value,set_signal,component_exists,pin_has_writer, signal management) implemented entirely onhal_get_p/hal_set_p/hal_get_s/hal_set_s/hal_comp_by_name. Nohal_priv.h, no direct shmem access, no re-implemented parsers.src/hal/halpybind.cc- pybind11 module (halpp.so), adapted from @rene-dev's bindings, extended with params, ports, streams and error paths. Type and direction tags are the_hal.Type/_hal.DirIntEnum classes, the same objectshal.queryuses: arguments accept members or plain ints, results come back as members. Port pins take and returnbytes(stris written as UTF-8);read()/peek()returnNonewhen fewer than the requested bytes are readable. Built alongside_hal/hal.py; module replacement is a separate (3.0) discussion.tests/halpp/- Python and native C++ smoke suites, plus a stream create/attach pair across two processes.Safety properties worth noting
Removed compared to the earlier revision of this PR
hal_comp,hal_pin<T>,hal_dir,PyPin): the oldhal.hhwas removed from master and nothing in the tree uses those names.HAL_*integer constants in halpp:halpp.Type/halpp.Dirreplace them.Relationship to the original pybind11 branch
Kept: the variant/map/PyPin structure and the binding shape.
Dropped: the private
set_commonstring parser (superseded byhal_set_pwithsetps_common_cb),hal_mutex_guard, theget_info_signalsstub,waitWritable(was non-functional).Test evidence
Full tree builds.
tests/halpp,tests/halmodule/*,tests/hal-*,tests/halrun-*,tests/halcompileandtests/build/header-sanitypass locally (23/23). The smoke suites cover component lifecycle, typed pins/params (negative and full 64-bit values), signals, linking, by-name get/set with coercion and text parsing, range errors, port pins (unlinked, sizing, resize refusal, read/peek/commit/write/clear, overfull writes), streams and error paths.tests/halppcompiles against the tree, so it skips underSYSTEM_BUILD(checked). cppcheck is clean on the new files.