From eb723c44a241b7b105a7bd949f1bd792e0b2a13a Mon Sep 17 00:00:00 2001 From: Xudong Han <42986190+han-xudong@users.noreply.github.com> Date: Sat, 1 Aug 2026 12:18:59 +0800 Subject: [PATCH 1/2] Update core snapshot to netft-cpp 0.3.2 --- CHANGELOG.md | 4 +-- README.md | 2 +- core/CMakeLists.txt | 21 +---------- core/SNAPSHOT.sha256 | 12 +++---- core/UPSTREAM | 4 +-- core/include/netft/client.hpp | 4 +++ core/src/client.cpp | 62 ++++++++++++++++++++++++++++++++- core/src/detail/client_impl.cpp | 5 +++ core/src/detail/client_impl.hpp | 1 + 9 files changed, 83 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad7bf07..3579be4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,8 @@ All notable changes to this project are documented in this file. ### Fixed -- Update the pinned native core to netft-cpp 0.3.1 so fail-stop clients do not receive - stalled or backward FT-sequence samples before the corresponding fault. +- Update the pinned native core to netft-cpp 0.3.2, including lifecycle-safe + client destruction from a sample callback. - Exclude the upstream legacy CLI sources from the Python package's private core snapshot. ## 2.1.0 - 2026-07-29 diff --git a/README.md b/README.md index b813413..3b4121c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ pyNetFT is a synchronous, typed Python client for ATI Industrial Automation Net F/T Ethernet force/torque sensors. It discovers the sensor calibration, streams RDT measurements through a native C++ core, and exposes raw counts, physical measurements, health, and recovery information without requiring NumPy. -- **Tested native core:** the protocol and recovery implementation is pinned to [netft-cpp 0.3.1](https://github.com/netft/netft-cpp/releases/tag/v0.3.1) and is exercised with offline fake sensors and native sanitizers. +- **Tested native core:** the protocol and recovery implementation is pinned to [netft-cpp 0.3.2](https://github.com/netft/netft-cpp/releases/tag/v0.3.2) and is exercised with offline fake sensors and native sanitizers. - **Sensor-aware data:** calibration, force and torque units, configuration revisions, sequence progress, and faults remain visible to the application. - **Self-contained typed wheels:** Linux, macOS, and Windows wheels include the native core and a minimal static HTTP-only curl build, plus inline type information for Python 3.10–3.14. diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 7b816fd..7e7b9a4 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,13 +1,12 @@ cmake_minimum_required(VERSION 3.16) -project(netft VERSION 0.3.1 LANGUAGES CXX) +project(netft VERSION 0.3.2 LANGUAGES CXX) include(CTest) include(GNUInstallDirs) include(CMakePackageConfigHelpers) option(NETFT_SANITIZERS "Enable address and undefined behavior sanitizers" OFF) -option(NETFT_BUILD_CLI "Build the netft command-line tool" ON) add_library(netft_sanitizers INTERFACE) if(NETFT_SANITIZERS) @@ -68,17 +67,6 @@ target_link_libraries(netft $ ) -if(NETFT_BUILD_CLI) - add_library(netft_cli_lib STATIC app/cli.cpp) - target_compile_features(netft_cli_lib PUBLIC cxx_std_17) - target_include_directories(netft_cli_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/app) - target_link_libraries(netft_cli_lib PUBLIC netft::netft) - - add_executable(netft_cli app/main.cpp) - set_target_properties(netft_cli PROPERTIES OUTPUT_NAME netft) - target_link_libraries(netft_cli PRIVATE netft_cli_lib) -endif() - install( DIRECTORY include/netft DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} @@ -90,13 +78,6 @@ install( LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} ) -if(NETFT_BUILD_CLI) - install( - TARGETS netft_cli - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - ) -endif() - set(netft_cmake_install_dir "${CMAKE_INSTALL_LIBDIR}/cmake/netft") configure_package_config_file( cmake/netftConfig.cmake.in diff --git a/core/SNAPSHOT.sha256 b/core/SNAPSHOT.sha256 index 3922b0d..f541e67 100644 --- a/core/SNAPSHOT.sha256 +++ b/core/SNAPSHOT.sha256 @@ -1,15 +1,15 @@ -c493d94026a5eb53f0589341d33915b8180c34bd712f2f9f2191e41ca48dd8f5 CMakeLists.txt +afaae562058f95e2464addc7408e2f7724d5f2f4559f8aa972156ddd30a3a5aa CMakeLists.txt c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 LICENSE -e78f98877ffe87b93dcec3d30d9c59337d3fa0e2e473ecc7134994a0c3bb7092 UPSTREAM +34abaa07479024a66926ea799193ac6b0ddd162895612889779d4700e66e12f7 UPSTREAM 40701f04e67bb18052b6c8f1db60cf5d41baab09e5396f5a88c1eb14ad1a7139 cmake/netftConfig.cmake.in -ebb4c53b7e7e65880580a7649e1c1a142e3a9db1fdec7d108d2d7dde0ad0d36d include/netft/client.hpp +e4a52fe7c7e13de95a29bdbd688d295a4a8ddc1805276274b0c14d4092f7253d include/netft/client.hpp 4f7fb383da5456fd8477e1981f4ff3eba9b82ec3a98f3c395e7d482faf75c112 include/netft/discovery.hpp 1beed1a22ad16aa801e0cceb0950548a32d263e01f260a86e72ec58d8f8d1a74 include/netft/export.hpp 912892bcb8bb5f95392c9c74983d91aab103b7adc5735505b0ac97e1012893b1 include/netft/status.hpp a7c899858aa7a0a28780504f14c1ecdaebb375607119a03500903f3782f45a68 include/netft/types.hpp -e8a68945aaebfd19a1cb1f5cf71f4666d997fa351a41e9adcd137b50a7eb3ae7 src/client.cpp -a7e0bcba7a8cd5ccb1ab04658ea735f2b6a3f2bac81c3bce95578e3c14349650 src/detail/client_impl.cpp -5d194b6b6cb8b04e742ee3e1508ac7b8e7972b5bbc7a02c94003d1f402dfccf1 src/detail/client_impl.hpp +1176a5babdc8623a3930cb0c4a1d844ccd864ff9edc86e8352f129c003dcaa62 src/client.cpp +eb2d243b42b3beaa859426eeea6b6a6e1ab4bd2b969ac6c5eb4fa3f662722760 src/detail/client_impl.cpp +d27110f90d7626ea575935147539aa56499a1bd1e33a626bfd9f650705c8f859 src/detail/client_impl.hpp ad8744385642693bb4666c428a5dbd0d6eff350b0079a6c9ff720512e89b6a53 src/detail/fault_latch.cpp be488c557e17044948cd62e6de07bf54862d389a584b40b78336479cecdaf23c src/detail/fault_latch.hpp f5953a7fc4fd5eee56f7aa3568fe7417010e3e767cfbb0ee5922ee5f8aa2a9aa src/detail/protocol.cpp diff --git a/core/UPSTREAM b/core/UPSTREAM index 45b22aa..d88d17d 100644 --- a/core/UPSTREAM +++ b/core/UPSTREAM @@ -1,4 +1,4 @@ repository=https://github.com/netft/netft-cpp -tag=v0.3.1 -commit=859eeda8b077093f9bc49d9c1e5506c334647e7b +tag=v0.3.2 +commit=95f0e721b885a2fc88e028f41987607747a42085 paths=CMakeLists.txt,LICENSE,cmake,include,src diff --git a/core/include/netft/client.hpp b/core/include/netft/client.hpp index 4a24373..772a79f 100644 --- a/core/include/netft/client.hpp +++ b/core/include/netft/client.hpp @@ -21,6 +21,10 @@ class NETFT_API Client { using SampleCallback = std::function; explicit Client(Config config); + // Destruction is synchronous except when initiated by the sample callback. + // In that case worker shutdown and storage reclamation are deferred because + // a callback cannot join its own thread. The callback must not access the + // Client after initiating destruction. ~Client(); Client(const Client &) = delete; Client &operator=(const Client &) = delete; diff --git a/core/src/client.cpp b/core/src/client.cpp index e253b28..98032f5 100644 --- a/core/src/client.cpp +++ b/core/src/client.cpp @@ -1,14 +1,74 @@ #include "netft/client.hpp" +#include +#include +#include +#include #include #include "detail/client_impl.hpp" namespace netft { +namespace { + +template class DeferredDestroyer { +public: + DeferredDestroyer() : worker_(&DeferredDestroyer::run, this) {} + + ~DeferredDestroyer() { + { + std::scoped_lock lock(mutex_); + stopping_ = true; + } + condition_.notify_one(); + worker_.join(); + } + + void enqueue(std::unique_ptr value) { + { + std::scoped_lock lock(mutex_); + pending_.push_back(std::move(value)); + } + condition_.notify_one(); + } + +private: + void run() noexcept { + for (;;) { + std::unique_ptr value; + { + std::unique_lock lock(mutex_); + condition_.wait(lock, [&] { return stopping_ || !pending_.empty(); }); + if (pending_.empty()) { + return; + } + value = std::move(pending_.front()); + pending_.pop_front(); + } + value->stop(); + } + } + + std::mutex mutex_; + std::condition_variable condition_; + std::deque> pending_; + bool stopping_{}; + std::thread worker_; +}; + +} // namespace Client::Client(Config config) : impl_(std::make_unique(std::move(config))) {} -Client::~Client() { stop(); } +Client::~Client() { + if (impl_ && impl_->called_from_worker_thread()) { + impl_->stop(); + static DeferredDestroyer deferred; + deferred.enqueue(std::move(impl_)); + return; + } + stop(); +} void Client::start(SampleCallback callback) { impl_->start(std::move(callback)); } diff --git a/core/src/detail/client_impl.cpp b/core/src/detail/client_impl.cpp index 5ebc20c..9387057 100644 --- a/core/src/detail/client_impl.cpp +++ b/core/src/detail/client_impl.cpp @@ -286,6 +286,11 @@ void Client::Impl::stop() noexcept { } } +bool Client::Impl::called_from_worker_thread() const noexcept { + std::scoped_lock lifecycle_lock(lifecycle_mutex_); + return worker_.joinable() && worker_.get_id() == std::this_thread::get_id(); +} + void Client::Impl::bias() { std::scoped_lock command_lock(command_mutex_); std::scoped_lock record_lock(record_mutex_); diff --git a/core/src/detail/client_impl.hpp b/core/src/detail/client_impl.hpp index 512ea21..7625449 100644 --- a/core/src/detail/client_impl.hpp +++ b/core/src/detail/client_impl.hpp @@ -24,6 +24,7 @@ class NETFT_LOCAL Client::Impl { void start(SampleCallback callback); void stop() noexcept; + [[nodiscard]] bool called_from_worker_thread() const noexcept; void bias(); bool wait_for_first_sample(std::chrono::duration timeout); bool faulted() const noexcept; From 74d47fdac3f5b3146898ad92f2eaf328d7bc3760 Mon Sep 17 00:00:00 2001 From: Xudong Han <42986190+han-xudong@users.noreply.github.com> Date: Sat, 1 Aug 2026 14:32:37 +0800 Subject: [PATCH 2/2] Update core snapshot to netft-cpp 0.3.3 --- .github/workflows/ci.yml | 1 - CHANGELOG.md | 2 +- CMakeLists.txt | 1 - README.md | 2 +- core/CMakeLists.txt | 2 +- core/SNAPSHOT.sha256 | 10 +++++----- core/UPSTREAM | 4 ++-- core/src/client.cpp | 25 +++++++++---------------- core/src/detail/client_impl.cpp | 5 ----- core/src/detail/client_impl.hpp | 6 +++++- 10 files changed, 24 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a6332d..6b9dc0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,7 +156,6 @@ jobs: -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} -DBUILD_TESTING=OFF - -DNETFT_BUILD_CLI=OFF -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/build/cmake-316-install - name: Build and install core run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 3579be4..462d2f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project are documented in this file. ### Fixed -- Update the pinned native core to netft-cpp 0.3.2, including lifecycle-safe +- Update the pinned native core to netft-cpp 0.3.3, including lifecycle-safe client destruction from a sample callback. - Exclude the upstream legacy CLI sources from the Python package's private core snapshot. diff --git a/CMakeLists.txt b/CMakeLists.txt index 379d2de..90a9232 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,6 @@ option(PYNETFT_BUILD_TESTING "Build pyNetFT native tests" OFF) set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) set(BUILD_TESTING OFF CACHE BOOL "" FORCE) -set(NETFT_BUILD_CLI OFF CACHE BOOL "" FORCE) set(CMAKE_POSITION_INDEPENDENT_CODE ON) find_package(Python COMPONENTS Interpreter Development.Module REQUIRED) diff --git a/README.md b/README.md index 3b4121c..fcea154 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ pyNetFT is a synchronous, typed Python client for ATI Industrial Automation Net F/T Ethernet force/torque sensors. It discovers the sensor calibration, streams RDT measurements through a native C++ core, and exposes raw counts, physical measurements, health, and recovery information without requiring NumPy. -- **Tested native core:** the protocol and recovery implementation is pinned to [netft-cpp 0.3.2](https://github.com/netft/netft-cpp/releases/tag/v0.3.2) and is exercised with offline fake sensors and native sanitizers. +- **Tested native core:** the protocol and recovery implementation is pinned to [netft-cpp 0.3.3](https://github.com/netft/netft-cpp/releases/tag/v0.3.3) and is exercised with offline fake sensors and native sanitizers. - **Sensor-aware data:** calibration, force and torque units, configuration revisions, sequence progress, and faults remain visible to the application. - **Self-contained typed wheels:** Linux, macOS, and Windows wheels include the native core and a minimal static HTTP-only curl build, plus inline type information for Python 3.10–3.14. diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 7e7b9a4..b24722c 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.16) -project(netft VERSION 0.3.2 LANGUAGES CXX) +project(netft VERSION 0.3.3 LANGUAGES CXX) include(CTest) include(GNUInstallDirs) diff --git a/core/SNAPSHOT.sha256 b/core/SNAPSHOT.sha256 index f541e67..6006ee5 100644 --- a/core/SNAPSHOT.sha256 +++ b/core/SNAPSHOT.sha256 @@ -1,15 +1,15 @@ -afaae562058f95e2464addc7408e2f7724d5f2f4559f8aa972156ddd30a3a5aa CMakeLists.txt +3db6fdfbb62d6073fa3f77784235cfe2eff06a1f102fb1d0f3a3f6da02dafa01 CMakeLists.txt c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 LICENSE -34abaa07479024a66926ea799193ac6b0ddd162895612889779d4700e66e12f7 UPSTREAM +926aedecaf33e0d9a178360ea455ee9cbdd54537f89d75835409fdb28fdcd908 UPSTREAM 40701f04e67bb18052b6c8f1db60cf5d41baab09e5396f5a88c1eb14ad1a7139 cmake/netftConfig.cmake.in e4a52fe7c7e13de95a29bdbd688d295a4a8ddc1805276274b0c14d4092f7253d include/netft/client.hpp 4f7fb383da5456fd8477e1981f4ff3eba9b82ec3a98f3c395e7d482faf75c112 include/netft/discovery.hpp 1beed1a22ad16aa801e0cceb0950548a32d263e01f260a86e72ec58d8f8d1a74 include/netft/export.hpp 912892bcb8bb5f95392c9c74983d91aab103b7adc5735505b0ac97e1012893b1 include/netft/status.hpp a7c899858aa7a0a28780504f14c1ecdaebb375607119a03500903f3782f45a68 include/netft/types.hpp -1176a5babdc8623a3930cb0c4a1d844ccd864ff9edc86e8352f129c003dcaa62 src/client.cpp -eb2d243b42b3beaa859426eeea6b6a6e1ab4bd2b969ac6c5eb4fa3f662722760 src/detail/client_impl.cpp -d27110f90d7626ea575935147539aa56499a1bd1e33a626bfd9f650705c8f859 src/detail/client_impl.hpp +7199f02fa0fd88059323f507edbf073b2a4827c692d1fd824893cbad8ea9215b src/client.cpp +a7e0bcba7a8cd5ccb1ab04658ea735f2b6a3f2bac81c3bce95578e3c14349650 src/detail/client_impl.cpp +b202cc6e250866e681d16b480911f77d865d9187300111f8482bc4a8b0424b34 src/detail/client_impl.hpp ad8744385642693bb4666c428a5dbd0d6eff350b0079a6c9ff720512e89b6a53 src/detail/fault_latch.cpp be488c557e17044948cd62e6de07bf54862d389a584b40b78336479cecdaf23c src/detail/fault_latch.hpp f5953a7fc4fd5eee56f7aa3568fe7417010e3e767cfbb0ee5922ee5f8aa2a9aa src/detail/protocol.cpp diff --git a/core/UPSTREAM b/core/UPSTREAM index d88d17d..c7bc616 100644 --- a/core/UPSTREAM +++ b/core/UPSTREAM @@ -1,4 +1,4 @@ repository=https://github.com/netft/netft-cpp -tag=v0.3.2 -commit=95f0e721b885a2fc88e028f41987607747a42085 +tag=v0.3.3 +commit=3259b8576b16fb150b51e9ef9090366c5e6efcba paths=CMakeLists.txt,LICENSE,cmake,include,src diff --git a/core/src/client.cpp b/core/src/client.cpp index 98032f5..4f56e79 100644 --- a/core/src/client.cpp +++ b/core/src/client.cpp @@ -15,15 +15,6 @@ template class DeferredDestroyer { public: DeferredDestroyer() : worker_(&DeferredDestroyer::run, this) {} - ~DeferredDestroyer() { - { - std::scoped_lock lock(mutex_); - stopping_ = true; - } - condition_.notify_one(); - worker_.join(); - } - void enqueue(std::unique_ptr value) { { std::scoped_lock lock(mutex_); @@ -38,10 +29,7 @@ template class DeferredDestroyer { std::unique_ptr value; { std::unique_lock lock(mutex_); - condition_.wait(lock, [&] { return stopping_ || !pending_.empty(); }); - if (pending_.empty()) { - return; - } + condition_.wait(lock, [&] { return !pending_.empty(); }); value = std::move(pending_.front()); pending_.pop_front(); } @@ -52,10 +40,16 @@ template class DeferredDestroyer { std::mutex mutex_; std::condition_variable condition_; std::deque> pending_; - bool stopping_{}; std::thread worker_; }; +template DeferredDestroyer &deferred_destroyer() { + // Callbacks can outlive static teardown. Keeping this worker alive for the + // process lifetime avoids both destruction-order races and lost enqueues. + static auto *instance = new DeferredDestroyer; + return *instance; +} + } // namespace Client::Client(Config config) : impl_(std::make_unique(std::move(config))) {} @@ -63,8 +57,7 @@ Client::Client(Config config) : impl_(std::make_unique(std::move(config))) Client::~Client() { if (impl_ && impl_->called_from_worker_thread()) { impl_->stop(); - static DeferredDestroyer deferred; - deferred.enqueue(std::move(impl_)); + deferred_destroyer().enqueue(std::move(impl_)); return; } stop(); diff --git a/core/src/detail/client_impl.cpp b/core/src/detail/client_impl.cpp index 9387057..5ebc20c 100644 --- a/core/src/detail/client_impl.cpp +++ b/core/src/detail/client_impl.cpp @@ -286,11 +286,6 @@ void Client::Impl::stop() noexcept { } } -bool Client::Impl::called_from_worker_thread() const noexcept { - std::scoped_lock lifecycle_lock(lifecycle_mutex_); - return worker_.joinable() && worker_.get_id() == std::this_thread::get_id(); -} - void Client::Impl::bias() { std::scoped_lock command_lock(command_mutex_); std::scoped_lock record_lock(record_mutex_); diff --git a/core/src/detail/client_impl.hpp b/core/src/detail/client_impl.hpp index 7625449..77d9ad8 100644 --- a/core/src/detail/client_impl.hpp +++ b/core/src/detail/client_impl.hpp @@ -24,7 +24,11 @@ class NETFT_LOCAL Client::Impl { void start(SampleCallback callback); void stop() noexcept; - [[nodiscard]] bool called_from_worker_thread() const noexcept; + [[nodiscard]] bool called_from_worker_thread() const noexcept { + std::scoped_lock lifecycle_lock(lifecycle_mutex_); + return !worker_exited_.load(std::memory_order_acquire) && + active_worker_id_ == std::this_thread::get_id(); + } void bias(); bool wait_for_first_sample(std::chrono::duration timeout); bool faulted() const noexcept;