Skip to content

hal: C++ API (hal.hh) and pybind11 bindings on the new HAL API#4251

Draft
grandixximo wants to merge 4 commits into
LinuxCNC:masterfrom
grandixximo:halxx-unified
Draft

hal: C++ API (hal.hh) and pybind11 bindings on the new HAL API#4251
grandixximo wants to merge 4 commits into
LinuxCNC:masterfrom
grandixximo:halxx-unified

Conversation

@grandixximo

@grandixximo grandixximo commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

@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).

Based on master, fully additive: _hal/hal.py are untouched and nothing is replaced. The by-name query functions and HAL_PORT creation are compiled out when the query API header is absent and activate automatically when #4247 lands.

What this adds

  • src/hal/hal.hh - type-safe C++ layer, header-only, in namespace linuxcnc::hal:
    • hal::pin<T> typed handles via traits<T> over the rtapi_ types: rtapi_s32/rtapi_sint are distinct C++ types, so 32/64 accessor selection happens at compile time with zero cost. Handles re-read the shmem slot per access, so hal_link() rewrites stay visible (same semantics as C pin pointers). Handles are move-only; HAL objects are unique.
    • hal::pin_t variant + hal::anypin: runtime-typed access multiplexes on the variant tag (the stored-type pattern required for name-keyed collections), with the tag supplied by the query API.
    • hal::component: pins + params, newpin/newparam (typed and runtime-typed), add_pin for the struct-member idiom, item access, prefix handling.
    • ULAPI by-name functions (get_value, set_value, set_signal, component_exists, pin_has_writer, signal management) implemented entirely on hal_get_p/hal_set_p/hal_get_s/hal_set_s/hal_comp_by_name. No hal_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 and error paths. Built alongside _hal/hal.py; module replacement is a separate (3.0) discussion.
  • src/emc/task/taskclass.cc - converted to the new API (it was the only hal.hh consumer).
  • tests/halpp/ - Python and native C++ smoke suites (both green), plus python3-pybind11 build dep.

Safety properties worth noting

  • Query callbacks are strictly exception-free: conversion errors are reported by flag and thrown after the library releases the HAL mutex. (Throwing through a callback wedges the session-wide mutex; found the hard way, and tested.)
  • Set-side coercion uses the item's type with range checks; an overflow raises instead of truncating.
  • Typed access compiles to the same two-load sequence as the C API and the old pointer dereference, so there is no hot-path cost.

Relationship to the original pybind11 branch

Kept: the variant/map/PyPin structure and the binding shape.
Dropped: the private set_common string parser (superseded by hal_set_p), hal_mutex_guard, the get_info_signals stub, waitWritable (was non-functional).
Compatibility aliases (hal_comp, hal_pin<T>, hal_dir, PyPin) are provided so existing consumers of the old hal.hh keep working.

Test evidence

Full tree builds; tests/halmodule.0/.1 pass; new smoke suites cover component lifecycle, typed pins/params (incl. negative S32 and 64-bit values), signals, linking, by-name get/set with coercion, port pins, and error paths. The suites auto-skip query-API checks when built without the query API, so they run on both master and the #4247 tree.

@grandixximo

Copy link
Copy Markdown
Contributor Author

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.

Problem Solution
Handle is width-blind Type lives in traits<T> (compile time) or the variant tag (runtime, from q->pp.type)
hal_link rewrites the handle slot pin<T> stores a pointer to the slot and re-reads per access (C semantics, staleness impossible)
Throwing through query callbacks wedges the HAL mutex session-wide Exceptions captured in the callback, rethrown after the library releases the mutex
Handle storage must be shmem hal_malloc'd hal_refs_u slot per item (halmodule's own pattern)

@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.

Comment thread src/hal/hal.hh Outdated
Comment thread src/hal/hal.hh Outdated
Comment thread src/hal/hal.hh Outdated
Comment thread src/hal/hal.hh Outdated
Comment thread src/hal/hal.hh Outdated
Comment thread src/hal/hal.hh Outdated
Comment thread src/hal/hal.hh
Comment thread src/hal/hal.hh Outdated
@rene-dev

Copy link
Copy Markdown
Member

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.

@grandixximo

Copy link
Copy Markdown
Contributor Author

Buster is basically not supported anymore with master, I will fix the review points tomorrow, thank you both ☺️
My bad on missing the include, getting late time for bed...

Reintroduce a C++ interface for HAL, built strictly on the public C
API and the user-land query API: no hal_priv.h, no direct shared
memory access, no re-implemented library internals.

- hal.hh: type-safe, header-only C++ layer. Typed pin/param handles
  via traits<T> (compile-time accessor selection for the rtapi_
  types), runtime-typed pin_t variant and anypin for name-based
  access, port wrapper, component class, and ULAPI by-name query/set
  functions when the query API is present.

  Handles re-read the shmem slot on every access so hal_link() slot
  rewrites stay visible (C pointer-variable semantics). Query callback
  paths are exception-free; errors are reported after the library
  releases the HAL mutex.

- halpybind.cc: pybind11 module (halpp.so) exposing component, Pin,
  enums and the by-name functions. Built alongside _hal/hal.py,
  replacing nothing.

- taskclass converted to the new API (it was the only hal.hh user).

- tests/halpp: Python and native C++ smoke suites.

Based on the pybind11 branch by rene-dev, re-based on the new HAL API.
@BsAtHome

Copy link
Copy Markdown
Contributor

There is another issue. The new hal_lib uses a reference counted init/exit and has separated out hal_lib_init() and hal_lib_exit(). The reason to take this out is that you do not need a component for pin/param/signal read/write or query interactions. You only need to have mapped shared memory.

Calling hal_init() to create a component (if you want to create pins) now calls the factored out hal_lib_init() and hal_exit()calls the factored out hal_lib_exit().

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).

@rene-dev

Copy link
Copy Markdown
Member

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?

Comment thread src/hal/hal.hh Outdated
Comment thread src/hal/hal.hh Outdated
Comment thread src/hal/hal.hh Outdated
Comment thread src/hal/hal.hh Outdated
Comment thread src/hal/hal.hh
Comment thread src/hal/hal.hh Outdated
Comment thread src/hal/hal.hh
Comment thread src/hal/hal.hh
Comment thread src/hal/hal.hh Outdated
Initialize the user-land HAL library at module import and register
hal_lib_exit() with Py_AtExit so forgotten component exits are
reported (query API tree only, where these exist).

String overloads of set_value/set_p/set_signal delegate text-to-value
conversion to setps_common_cb for consistency with halcmd setp/sets
(boolean names, locale-safe floats, range checks). setps_util.c is
linked via $(wildcard) so the build works with and without it.
- Ports transfer binary data: read/write/peek use std::vector<char>,
  bindings expose bytes
- Port pins have a scalar value: reads yield the queue reference (via
  hal_get_sint until hal_get_port() lands), writes are rejected
- Port creation is core hal lib; comment corrected, still gated on
  availability of hal_pin_new_port()
- By-name section documented user-space-only by definition
- Type constants use the new names HAL_BOOL/HAL_REAL throughout
- pin_has_writer uses hal_getref_p/hal_getref_s (no value read)
- get_value propagates the library error instead of a generic
  not-found
@grandixximo

Copy link
Copy Markdown
Contributor Author

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. get_info_pins/signals/params, pin_has_writer and component_exists are used by qtvcp (qt_halobjects, hal_selectionbox) and hal_glib, and they need the query API (hal_list_*, hal_get_p) from #4247. Without it, the options are a strict subset module (GUIs break immediately) or private shmem access (exactly what this API change abolishes).

Second, the compatibility tail: hal.error exception type, KeyboardInterrupt on halcmd unload (documented in hal.py's docstring, user comps wrap their main loop in it), the exact get_info_* dict shapes (tests/halmodule.0 compares verbatim), the hal.stream object API (tests/halmodule.1), setitem coercion quirks, pin .dir/.type attributes, and the hal.py wrapper shims. Matching all of that is what makes the swap a user-visible event, and it deserves its own PR with a full parity pass, not a rename here.

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...

Port handle type and creation semantics are still in flux (opaque
hal_port_t conflicts with the legacy form, hal_get_port is disabled,
hal_pin_new_port does not exist in the base tree), so a port wrapper
written now would be transitional glue that gets redone at the break.
Remove the port class, the variant alternative and the bindings;
document the exemption. Ports return post-break as a hal_port_t
based wrapper.
@rene-dev

Copy link
Copy Markdown
Member

I see that #4231 changes halmodule.cc, we should also add those functions via c++/pybind11

@BsAtHome

Copy link
Copy Markdown
Contributor

I see that #4231 changes halmodule.cc, we should also add those functions via c++/pybind11

No, all of this should be in halquery.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants