diff --git a/.cirrus.yml b/.cirrus.yml index b4cf20166..95167de78 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,7 +1,7 @@ alpine_task: container: image: alpine:latest - install_script: apk add autoconf automake g++ gcc libusb-dev libtool linux-headers eudev-dev make musl-dev + install_script: apk add autoconf automake g++ gcc libusb-dev libtool linux-headers eudev-libs make musl-dev script: - ./bootstrap - ./configure || { cat config.log; exit 1; } diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index db89e3e44..e44db3ba2 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -125,19 +125,22 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install autoconf automake libudev-dev libusb-1.0-0-dev pkg-config python3-pip ninja-build + sudo apt install autoconf automake libudev1 libusb-1.0-0-dev pkg-config python3-pip ninja-build sudo -H pip3 install meson - name: Configure CMake run: | rm -rf build install - cmake -B build/shared -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON -DHIDAPI_WITH_TESTS=ON "-DCMAKE_C_FLAGS=${GNU_COMPILE_FLAGS}" - cmake -B build/static -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/static -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${GNU_COMPILE_FLAGS}" + cmake -B build/shared -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DHIDAPI_WITH_TESTS=ON -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${GNU_COMPILE_FLAGS}" + cmake -B build/static -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DHIDAPI_WITH_TESTS=ON -DCMAKE_INSTALL_PREFIX=install/static -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${GNU_COMPILE_FLAGS}" - name: Build CMake Shared working-directory: build/shared run: make install - name: Build CMake Static working-directory: build/static run: make install + - name: Test CMake Static + working-directory: build/static + run: ctest --output-on-failure - name: Check pkg-config package run: | autoreconf --install --force hidapisrc/tests/pkg-config @@ -581,12 +584,12 @@ jobs: path: hidapisrc - name: Install dependencies run: | - apk add gcc musl-dev autoconf automake libtool eudev-dev libusb-dev linux-headers cmake ninja make + apk add gcc musl-dev autoconf automake libtool eudev-libs libusb-dev linux-headers cmake ninja make - name: Configure CMake run: | rm -rf build install - cmake -B build/shared-cmake -S hidapisrc -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install/shared-cmake -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${ALPINE_COMPILE_FLAGS}" - cmake -B build/static-cmake -S hidapisrc -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install/static-cmake -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${ALPINE_COMPILE_FLAGS}" + cmake -B build/shared-cmake -S hidapisrc -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DCMAKE_INSTALL_PREFIX=install/shared-cmake -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${ALPINE_COMPILE_FLAGS}" + cmake -B build/static-cmake -S hidapisrc -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DCMAKE_INSTALL_PREFIX=install/static-cmake -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${ALPINE_COMPILE_FLAGS}" - name: Configure Automake working-directory: hidapisrc run: | @@ -595,9 +598,15 @@ jobs: - name: Build CMake Shared working-directory: build/shared-cmake run: ninja install + - name: Test CMake Shared + working-directory: build/shared-cmake + run: ctest --output-on-failure - name: Build CMake Static working-directory: build/static-cmake run: ninja install + - name: Test CMake Static + working-directory: build/static-cmake + run: ctest --output-on-failure - name: Build Automake working-directory: hidapisrc run: | diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 217853f5a..d3ac0489c 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -141,7 +141,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install libudev-dev libusb-1.0-0-dev ninja-build + sudo apt install libudev1 libusb-1.0-0-dev ninja-build - name: Configure run: | cmake -B build -S src -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_HIDTEST=ON -DCMAKE_C_COMPILER=gcc diff --git a/BUILD.md b/BUILD.md index d7a3546f6..1211d61d3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -45,14 +45,14 @@ Regardless of what build system you choose to use, there are specific dependenci ### Linux: Depending on which backend you're going to build, you'll need to install -additional development packages. For `linux/hidraw` backend, you need a -development package for `libudev`. For `libusb` backend, naturally, you need -`libusb` development package. +additional packages. The `linux/hidraw` backend loads `libudev` dynamically, +so it only needs the runtime library. The `libusb` backend needs the `libusb` +development package. On Debian/Ubuntu systems these can be installed by running: ```sh -# required only by hidraw backend -sudo apt install libudev-dev +# required at runtime by the hidraw backend +sudo apt install libudev1 # required only by libusb backend sudo apt install libusb-1.0-0-dev ``` diff --git a/configure.ac b/configure.ac index 70ba95c2d..d86a97fea 100644 --- a/configure.ac +++ b/configure.ac @@ -28,6 +28,8 @@ PKG_PROG_PKG_CONFIG m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) +HIDAPI_HIDRAW_PRIVATE_LIBS= + hidapi_lib_error() { echo "" echo " Library $1 was not found on this system." @@ -57,9 +59,13 @@ case $host in threads="pthreads" # HIDAPI/hidraw libs - PKG_CHECK_MODULES([libudev], [libudev], true, [hidapi_lib_error libudev]) - LIBS_HIDRAW_PR="${LIBS_HIDRAW_PR} $libudev_LIBS" - CFLAGS_HIDRAW="${CFLAGS_HIDRAW} $libudev_CFLAGS" + hidapi_saved_LIBS="$LIBS" + AC_SEARCH_LIBS([dlopen], [dl], [], [hidapi_lib_error libdl]) + if test "x$ac_cv_search_dlopen" != "xnone required"; then + LIBS_HIDRAW_PR="${LIBS_HIDRAW_PR} ${ac_cv_search_dlopen}" + HIDAPI_HIDRAW_PRIVATE_LIBS="${ac_cv_search_dlopen}" + fi + LIBS="$hidapi_saved_LIBS" # HIDAPI/libusb libs AC_CHECK_LIB([rt], [clock_gettime], [LIBS_LIBUSB_PRIVATE="${LIBS_LIBUSB_PRIVATE} -lrt"], [hidapi_lib_error librt]) @@ -243,33 +249,40 @@ m4_define([HIDAPI_CONFIGURE_BACKEND_PC], [ -e "s|@HIDAPI_BACKEND_NAME@|$2|g" \ -e "s|@HIDAPI_BACKEND_DESCRIPTION@|$3|g" \ -e "s|@HIDAPI_BACKEND_LIBRARY_NAME@|$4|g" \ + -e "s|@HIDAPI_BACKEND_PRIVATE_LIBS@|$5|g" \ "$1" > "$1.tmp" && mv "$1.tmp" "$1" - ], [$5]) + ], [$6]) ]) HIDAPI_LIB_NAME=hidapi HIDAPI_LIBUSB_LIB_NAME=hidapi +HIDAPI_PRIVATE_LIBS= if test "x$os" = "xlinux"; then HIDAPI_LIB_NAME=hidapi-hidraw HIDAPI_LIBUSB_LIB_NAME=hidapi-libusb + HIDAPI_PRIVATE_LIBS="$HIDAPI_HIDRAW_PRIVATE_LIBS" HIDAPI_CONFIGURE_BACKEND_PC( [pc/hidapi-hidraw.pc], [hidapi-hidraw], [C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the hidraw implementation.], - [hidapi-hidraw]) + [hidapi-hidraw], + [$HIDAPI_HIDRAW_PRIVATE_LIBS], + [HIDAPI_HIDRAW_PRIVATE_LIBS="$HIDAPI_HIDRAW_PRIVATE_LIBS"]) elif test "x$os" = "xdarwin"; then HIDAPI_CONFIGURE_BACKEND_PC( [pc/hidapi-darwin.pc], [hidapi-darwin], [C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the Darwin implementation.], - [hidapi]) + [hidapi], + []) elif test "x$os" = "xwindows"; then HIDAPI_CONFIGURE_BACKEND_PC( [pc/hidapi-winapi.pc], [hidapi-winapi], [C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the WinAPI implementation.], - [hidapi]) + [hidapi], + []) fi if test "x$os" != "xdarwin" && test "x$os" != "xwindows"; then HIDAPI_CONFIGURE_BACKEND_PC( @@ -277,11 +290,13 @@ if test "x$os" != "xdarwin" && test "x$os" != "xwindows"; then [hidapi-libusb], [C Library for USB HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the libusb implementation.], [$hidapi_libusb_lib_name], + [], [hidapi_libusb_lib_name="$HIDAPI_LIBUSB_LIB_NAME"]) fi m4_undefine([HIDAPI_CONFIGURE_BACKEND_PC]) AC_SUBST([HIDAPI_LIB_NAME]) +AC_SUBST([HIDAPI_PRIVATE_LIBS]) AC_CONFIG_FILES([pc/hidapi.pc]) AC_SUBST(LTLDFLAGS) diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 1c34e09d2..15a4dca22 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -9,10 +9,7 @@ target_link_libraries(hidapi_hidraw PUBLIC hidapi_include) find_package(Threads REQUIRED) -include(FindPkgConfig) -pkg_check_modules(libudev REQUIRED IMPORTED_TARGET libudev) - -target_link_libraries(hidapi_hidraw PRIVATE PkgConfig::libudev Threads::Threads) +target_link_libraries(hidapi_hidraw PRIVATE Threads::Threads ${CMAKE_DL_LIBS}) set_target_properties(hidapi_hidraw PROPERTIES @@ -28,6 +25,10 @@ add_library(hidapi::hidraw ALIAS hidapi_hidraw) # compatibility with raw library link add_library(hidapi-hidraw ALIAS hidapi_hidraw) +if(HIDAPI_WITH_TESTS) + add_subdirectory(test) +endif() + if(HIDAPI_INSTALL_TARGETS) install(TARGETS hidapi_hidraw EXPORT hidapi LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" @@ -40,4 +41,5 @@ hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi-backend-specific.pc.in" OUTPUT_NAME "hidapi-hidraw" LIBRARY_NAME "hidapi-hidraw" DESCRIPTION "C Library for USB/Bluetooth HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the hidraw implementation." + PRIVATE_LIBS "-ldl" ) diff --git a/linux/Makefile-manual b/linux/Makefile-manual index 81d28cf2c..357df50c8 100644 --- a/linux/Makefile-manual +++ b/linux/Makefile-manual @@ -18,18 +18,17 @@ LDFLAGS ?= -Wall -g COBJS = hid.o ../hidtest/test.o OBJS = $(COBJS) -LIBS_UDEV = `pkg-config libudev --libs` -lrt -LIBS = $(LIBS_UDEV) -INCLUDES ?= -I../hidapi `pkg-config libusb-1.0 --cflags` +LIBS_HIDRAW = -ldl +INCLUDES ?= -I../hidapi # Console Test Program hidtest-hidraw: $(COBJS) - $(CC) $(LDFLAGS) $^ $(LIBS_UDEV) -o $@ + $(CC) $(LDFLAGS) $^ $(LIBS_HIDRAW) -o $@ # Shared Libs libhidapi-hidraw.so: $(COBJS) - $(CC) $(LDFLAGS) $(LIBS_UDEV) -shared -fpic -Wl,-soname,$@.0 $^ -o $@ + $(CC) $(LDFLAGS) -shared -fpic -Wl,-soname,$@.0 $^ $(LIBS_HIDRAW) -o $@ # Objects $(COBJS): %.o: %.c diff --git a/linux/Makefile.am b/linux/Makefile.am index 230eeb75a..1ef290b69 100644 --- a/linux/Makefile.am +++ b/linux/Makefile.am @@ -7,4 +7,9 @@ libhidapi_hidraw_la_LIBADD = $(LIBS_HIDRAW) hdrdir = $(includedir)/hidapi hdr_HEADERS = $(top_srcdir)/hidapi/hidapi.h -EXTRA_DIST = Makefile-manual +EXTRA_DIST = \ + Makefile-manual \ + test/CMakeLists.txt \ + test/dlopen_interposer.c \ + test/hidapi_hidraw_loader_test.c \ + test/incomplete_udev.c diff --git a/linux/hid.c b/linux/hid.c index 54165661c..07b820df4 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -7,7 +7,7 @@ libusb/hidapi Team - Copyright 2022, All Rights Reserved. + Copyright 2026, All Rights Reserved. At the discretion of the user of this library, this software may be licensed under the terms of the @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -40,11 +41,51 @@ /* Linux */ #include #include -#include #include "hidapi.h" +struct udev; struct udev_device; +struct udev_enumerate; +struct udev_list_entry; + +typedef struct udev *udev_new_(void); +typedef struct udev *udev_unref_(struct udev *udev); +typedef struct udev_device *udev_device_new_from_devnum_(struct udev *udev, char type, dev_t devnum); +typedef struct udev_device *udev_device_new_from_syspath_(struct udev *udev, const char *syspath); +typedef struct udev_device *udev_device_unref_(struct udev_device *udev_device); +typedef struct udev_device *udev_device_get_parent_with_subsystem_devtype_( + struct udev_device *udev_device, const char *subsystem, const char *devtype); +typedef const char *udev_device_get_syspath_(struct udev_device *udev_device); +typedef const char *udev_device_get_devnode_(struct udev_device *udev_device); +typedef const char *udev_device_get_sysattr_value_(struct udev_device *udev_device, const char *sysattr); +typedef struct udev_enumerate *udev_enumerate_new_(struct udev *udev); +typedef struct udev_enumerate *udev_enumerate_unref_(struct udev_enumerate *udev_enumerate); +typedef int udev_enumerate_add_match_subsystem_(struct udev_enumerate *udev_enumerate, const char *subsystem); +typedef int udev_enumerate_scan_devices_(struct udev_enumerate *udev_enumerate); +typedef struct udev_list_entry *udev_enumerate_get_list_entry_(struct udev_enumerate *udev_enumerate); +typedef struct udev_list_entry *udev_list_entry_get_next_(struct udev_list_entry *list_entry); +typedef const char *udev_list_entry_get_name_(struct udev_list_entry *list_entry); + +static udev_new_ *udev_new = NULL; +static udev_unref_ *udev_unref = NULL; +static udev_device_new_from_devnum_ *udev_device_new_from_devnum = NULL; +static udev_device_new_from_syspath_ *udev_device_new_from_syspath = NULL; +static udev_device_unref_ *udev_device_unref = NULL; +static udev_device_get_parent_with_subsystem_devtype_ *udev_device_get_parent_with_subsystem_devtype = NULL; +static udev_device_get_syspath_ *udev_device_get_syspath = NULL; +static udev_device_get_devnode_ *udev_device_get_devnode = NULL; +static udev_device_get_sysattr_value_ *udev_device_get_sysattr_value = NULL; +static udev_enumerate_new_ *udev_enumerate_new = NULL; +static udev_enumerate_unref_ *udev_enumerate_unref = NULL; +static udev_enumerate_add_match_subsystem_ *udev_enumerate_add_match_subsystem = NULL; +static udev_enumerate_scan_devices_ *udev_enumerate_scan_devices = NULL; +static udev_enumerate_get_list_entry_ *udev_enumerate_get_list_entry = NULL; +static udev_list_entry_get_next_ *udev_list_entry_get_next = NULL; +static udev_list_entry_get_name_ *udev_list_entry_get_name = NULL; + +static void *udev_lib_handle = NULL; +static int hidapi_initialized = 0; #ifdef HIDAPI_ALLOW_BUILD_WORKAROUND_KERNEL_2_6_39 /* This definitions first appeared in Linux Kernel 2.6.39 in linux/hidraw.h. @@ -168,6 +209,84 @@ static void register_global_error_format(const char *format, ...) va_end(args); } +static void free_udev_library(void) +{ + if (udev_lib_handle) + dlclose(udev_lib_handle); + udev_lib_handle = NULL; + + udev_new = NULL; + udev_unref = NULL; + udev_device_new_from_devnum = NULL; + udev_device_new_from_syspath = NULL; + udev_device_unref = NULL; + udev_device_get_parent_with_subsystem_devtype = NULL; + udev_device_get_syspath = NULL; + udev_device_get_devnode = NULL; + udev_device_get_sysattr_value = NULL; + udev_enumerate_new = NULL; + udev_enumerate_unref = NULL; + udev_enumerate_add_match_subsystem = NULL; + udev_enumerate_scan_devices = NULL; + udev_enumerate_get_list_entry = NULL; + udev_list_entry_get_next = NULL; + udev_list_entry_get_name = NULL; +} + +static int lookup_udev_functions(void) +{ + udev_lib_handle = dlopen("libudev.so.1", RTLD_NOW | RTLD_LOCAL); + if (!udev_lib_handle) { + const char *error = dlerror(); + register_global_error_format("Failed to load libudev.so.1: %s", error ? error : "unknown error"); + return -1; + } + +/* Avoid direct function-pointer casts from void pointers. + Using memcpy keeps this warning-free regardless of the compiler settings. */ +#define HIDAPI_UDEV_RESOLVE(name) do { \ + void *symbol; \ + const char *error; \ + dlerror(); \ + symbol = dlsym(udev_lib_handle, #name); \ + error = dlerror(); \ + if (error) { \ + register_global_error_format("Failed to resolve libudev symbol %s: %s", #name, error); \ + goto err; \ + } \ + if (sizeof(name) != sizeof(symbol)) { \ + register_global_error_format("Failed to resolve libudev symbol %s: incompatible pointer sizes", #name); \ + goto err; \ + } \ + memcpy(&name, &symbol, sizeof(symbol)); \ +} while (0) + + HIDAPI_UDEV_RESOLVE(udev_new); + HIDAPI_UDEV_RESOLVE(udev_unref); + HIDAPI_UDEV_RESOLVE(udev_device_new_from_devnum); + HIDAPI_UDEV_RESOLVE(udev_device_new_from_syspath); + HIDAPI_UDEV_RESOLVE(udev_device_unref); + HIDAPI_UDEV_RESOLVE(udev_device_get_parent_with_subsystem_devtype); + HIDAPI_UDEV_RESOLVE(udev_device_get_syspath); + HIDAPI_UDEV_RESOLVE(udev_device_get_devnode); + HIDAPI_UDEV_RESOLVE(udev_device_get_sysattr_value); + HIDAPI_UDEV_RESOLVE(udev_enumerate_new); + HIDAPI_UDEV_RESOLVE(udev_enumerate_unref); + HIDAPI_UDEV_RESOLVE(udev_enumerate_add_match_subsystem); + HIDAPI_UDEV_RESOLVE(udev_enumerate_scan_devices); + HIDAPI_UDEV_RESOLVE(udev_enumerate_get_list_entry); + HIDAPI_UDEV_RESOLVE(udev_list_entry_get_next); + HIDAPI_UDEV_RESOLVE(udev_list_entry_get_name); + +#undef HIDAPI_UDEV_RESOLVE + + return 0; + +err: + free_udev_library(); + return -1; +} + /* Set the last error for a device to be reported by hid_error(dev). * The given error message will be copied (and decoded according to the * currently locale, so do not pass in string constants). @@ -921,11 +1040,20 @@ int HID_API_EXPORT hid_init(void) if (!locale) setlocale(LC_CTYPE, ""); + if (!hidapi_initialized) { + if (lookup_udev_functions() < 0) + return -1; + hidapi_initialized = 1; + } + return 0; } int HID_API_EXPORT hid_exit(void) { + free_udev_library(); + hidapi_initialized = 0; + /* Free global error message */ register_global_error(NULL); @@ -941,7 +1069,8 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, struct hid_device_info *root = NULL; /* return object */ struct hid_device_info *cur_dev = NULL; - hid_init(); + if (hid_init() < 0) + return NULL; /* register_global_error: global error is reset by hid_init */ /* Create the udev object */ @@ -958,7 +1087,9 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, devices = udev_enumerate_get_list_entry(enumerate); /* For each item, see if it matches the vid/pid, and if so create a udev_device record for it */ - udev_list_entry_foreach(dev_list_entry, devices) { + for (dev_list_entry = devices; + dev_list_entry != NULL; + dev_list_entry = udev_list_entry_get_next(dev_list_entry)) { const char *sysfs_path; unsigned short dev_vid = 0; unsigned short dev_pid = 0; @@ -1080,7 +1211,8 @@ hid_device * HID_API_EXPORT hid_open_path(const char *path) { hid_device *dev = NULL; - hid_init(); + if (hid_init() < 0) + return NULL; /* register_global_error: global error is reset by hid_init */ dev = new_hid_device(); diff --git a/linux/test/CMakeLists.txt b/linux/test/CMakeLists.txt new file mode 100644 index 000000000..428b4065d --- /dev/null +++ b/linux/test/CMakeLists.txt @@ -0,0 +1,46 @@ +add_executable(hidapi_hidraw_loader_test + hidapi_hidraw_loader_test.c +) +target_link_libraries(hidapi_hidraw_loader_test PRIVATE hidapi_hidraw) +target_link_libraries(hidapi_hidraw_loader_test PRIVATE ${CMAKE_DL_LIBS}) + +add_library(hidapi_test_incomplete_udev SHARED + incomplete_udev.c +) + +add_library(hidapi_test_dlopen_interposer SHARED + dlopen_interposer.c +) +target_link_libraries(hidapi_test_dlopen_interposer PRIVATE ${CMAKE_DL_LIBS}) + +add_dependencies(hidapi_hidraw_loader_test + hidapi_test_incomplete_udev + hidapi_test_dlopen_interposer +) + +add_test( + NAME hidapi-hidraw-loader-reload + COMMAND + "${CMAKE_COMMAND}" -E env + "ASAN_OPTIONS=verify_asan_link_order=0" + "LD_PRELOAD=$" + "HIDAPI_TEST_FAIL_ATTEMPTS=0" + "$" reload +) + +add_test( + NAME hidapi-hidraw-loader-failure-recovery + COMMAND + "${CMAKE_COMMAND}" -E env + "ASAN_OPTIONS=verify_asan_link_order=0" + "LD_PRELOAD=$" + "HIDAPI_TEST_FAKE_UDEV=$" + "HIDAPI_TEST_FAIL_ATTEMPTS=2" + "$" failure-recovery +) + +set_tests_properties( + hidapi-hidraw-loader-reload + hidapi-hidraw-loader-failure-recovery + PROPERTIES TIMEOUT 30 +) diff --git a/linux/test/dlopen_interposer.c b/linux/test/dlopen_interposer.c new file mode 100644 index 000000000..93623c573 --- /dev/null +++ b/linux/test/dlopen_interposer.c @@ -0,0 +1,123 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +#define _GNU_SOURCE + +#include +#include +#include +#include + + +typedef void *dlopen_(const char *filename, int flags); +typedef int dlclose_(void *handle); + +static dlopen_ *real_dlopen; +static dlclose_ *real_dlclose; +static void *udev_handle; +static unsigned int udev_attempts; +static unsigned int udev_open_count; +static unsigned int udev_close_count; + +static int resolve_real_functions(void) +{ + void *symbol; + + if (!real_dlopen) { + symbol = dlsym(RTLD_NEXT, "dlopen"); + if (!symbol || sizeof(real_dlopen) != sizeof(symbol)) + return -1; + memcpy(&real_dlopen, &symbol, sizeof(symbol)); + } + + if (!real_dlclose) { + symbol = dlsym(RTLD_NEXT, "dlclose"); + if (!symbol || sizeof(real_dlclose) != sizeof(symbol)) + return -1; + memcpy(&real_dlclose, &symbol, sizeof(symbol)); + } + + return 0; +} + +static unsigned int failure_attempt_limit(void) +{ + const char *value = getenv("HIDAPI_TEST_FAIL_ATTEMPTS"); + char *end; + unsigned long limit; + + if (!value) + return 0; + + limit = strtoul(value, &end, 10); + if (*value == '\0' || *end != '\0' || limit > UINT_MAX) + return 0; + + return (unsigned int) limit; +} + +void *dlopen(const char *filename, int flags) +{ + void *handle; + const int is_udev = filename && strcmp(filename, "libudev.so.1") == 0; + + if (resolve_real_functions() < 0) + return NULL; + + if (is_udev && udev_attempts++ < failure_attempt_limit()) { + const char *fake_udev = getenv("HIDAPI_TEST_FAKE_UDEV"); + + if (!fake_udev) + return NULL; + handle = real_dlopen(fake_udev, flags); + } + else { + handle = real_dlopen(filename, flags); + } + + if (is_udev && handle) { + udev_handle = handle; + ++udev_open_count; + } + + return handle; +} + +int dlclose(void *handle) +{ + if (resolve_real_functions() < 0) + return -1; + + if (handle == udev_handle) { + udev_handle = NULL; + ++udev_close_count; + } + + return real_dlclose(handle); +} + +unsigned int hidapi_test_udev_open_count(void) +{ + return udev_open_count; +} + +unsigned int hidapi_test_udev_close_count(void) +{ + return udev_close_count; +} diff --git a/linux/test/hidapi_hidraw_loader_test.c b/linux/test/hidapi_hidraw_loader_test.c new file mode 100644 index 000000000..c9735217a --- /dev/null +++ b/linux/test/hidapi_hidraw_loader_test.c @@ -0,0 +1,161 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +#define _GNU_SOURCE + +#include +#include +#include +#include + +#include "hidapi.h" + + +typedef unsigned int counter_(void); + +static int read_counter(const char *name, unsigned int *value) +{ + counter_ *counter; + void *symbol = dlsym(RTLD_DEFAULT, name); + + if (!symbol) { + fprintf(stderr, "unable to resolve test counter %s: %s\n", name, dlerror()); + return -1; + } + if (sizeof(counter) != sizeof(symbol)) { + fprintf(stderr, "test counter pointer size mismatch\n"); + return -1; + } + memcpy(&counter, &symbol, sizeof(symbol)); + *value = counter(); + return 0; +} + +static int verify_balanced_cleanup(unsigned int initial_opens, + unsigned int initial_closes, + unsigned int expected_cycles) +{ + unsigned int final_opens; + unsigned int final_closes; + + if (read_counter("hidapi_test_udev_open_count", &final_opens) < 0 || + read_counter("hidapi_test_udev_close_count", &final_closes) < 0) + return 1; + + if (final_opens - initial_opens != expected_cycles || + final_closes - initial_closes != expected_cycles) { + fprintf(stderr, + "unbalanced libudev lifecycle: expected %u cycles, observed %u opens and %u closes\n", + expected_cycles, + final_opens - initial_opens, + final_closes - initial_closes); + return 1; + } + + return 0; +} + +static int test_reload(void) +{ + int i; + unsigned int initial_opens; + unsigned int initial_closes; + + if (read_counter("hidapi_test_udev_open_count", &initial_opens) < 0 || + read_counter("hidapi_test_udev_close_count", &initial_closes) < 0) + return 1; + + for (i = 0; i < 3; ++i) { + if (hid_init() != 0) { + const wchar_t *error = hid_error(NULL); + fwprintf(stderr, L"hid_init failed: %ls\n", error ? error : L"(null)"); + return 1; + } + if (hid_exit() != 0) { + fprintf(stderr, "hid_exit failed\n"); + return 1; + } + } + + return verify_balanced_cleanup(initial_opens, initial_closes, 3); +} + +static int test_failure_recovery(void) +{ + int i; + unsigned int initial_opens; + unsigned int initial_closes; + + if (read_counter("hidapi_test_udev_open_count", &initial_opens) < 0 || + read_counter("hidapi_test_udev_close_count", &initial_closes) < 0) + return 1; + + for (i = 0; i < 2; ++i) { + const wchar_t *error; + + if (hid_init() != -1) { + fprintf(stderr, "hid_init unexpectedly succeeded with an incomplete libudev\n"); + return 1; + } + + error = hid_error(NULL); + if (!error || !wcsstr(error, L"udev_list_entry_get_name")) { + fwprintf(stderr, L"hid_init returned an unexpected error: %ls\n", error ? error : L"(null)"); + return 1; + } + } + + if (hid_init() != 0) { + const wchar_t *error = hid_error(NULL); + fwprintf(stderr, L"hid_init did not recover: %ls\n", error ? error : L"(null)"); + return 1; + } + if (hid_exit() != 0) { + fprintf(stderr, "hid_exit failed after recovery\n"); + return 1; + } + + if (hid_init() != 0) { + const wchar_t *error = hid_error(NULL); + fwprintf(stderr, L"hid_init failed after reload: %ls\n", error ? error : L"(null)"); + return 1; + } + if (hid_exit() != 0) { + fprintf(stderr, "final hid_exit failed\n"); + return 1; + } + + return verify_balanced_cleanup(initial_opens, initial_closes, 4); +} + +int main(int argc, char **argv) +{ + if (argc != 2) { + fprintf(stderr, "usage: %s reload|failure-recovery\n", argv[0]); + return 2; + } + + if (strcmp(argv[1], "reload") == 0) + return test_reload(); + if (strcmp(argv[1], "failure-recovery") == 0) + return test_failure_recovery(); + + fprintf(stderr, "unknown test mode: %s\n", argv[1]); + return 2; +} diff --git a/linux/test/incomplete_udev.c b/linux/test/incomplete_udev.c new file mode 100644 index 000000000..dc51927ff --- /dev/null +++ b/linux/test/incomplete_udev.c @@ -0,0 +1,38 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +#define HIDAPI_UDEV_TEST_STUB(name) void name(void) {} + +HIDAPI_UDEV_TEST_STUB(udev_new) +HIDAPI_UDEV_TEST_STUB(udev_unref) +HIDAPI_UDEV_TEST_STUB(udev_device_new_from_devnum) +HIDAPI_UDEV_TEST_STUB(udev_device_new_from_syspath) +HIDAPI_UDEV_TEST_STUB(udev_device_unref) +HIDAPI_UDEV_TEST_STUB(udev_device_get_parent_with_subsystem_devtype) +HIDAPI_UDEV_TEST_STUB(udev_device_get_syspath) +HIDAPI_UDEV_TEST_STUB(udev_device_get_devnode) +HIDAPI_UDEV_TEST_STUB(udev_device_get_sysattr_value) +HIDAPI_UDEV_TEST_STUB(udev_enumerate_new) +HIDAPI_UDEV_TEST_STUB(udev_enumerate_unref) +HIDAPI_UDEV_TEST_STUB(udev_enumerate_add_match_subsystem) +HIDAPI_UDEV_TEST_STUB(udev_enumerate_scan_devices) +HIDAPI_UDEV_TEST_STUB(udev_enumerate_get_list_entry) +HIDAPI_UDEV_TEST_STUB(udev_list_entry_get_next) + +/* udev_list_entry_get_name is intentionally missing. */ diff --git a/pc/hidapi-backend-specific.pc.in b/pc/hidapi-backend-specific.pc.in index ecf1b6e7c..bcd4a1ddd 100644 --- a/pc/hidapi-backend-specific.pc.in +++ b/pc/hidapi-backend-specific.pc.in @@ -8,4 +8,5 @@ Description: @HIDAPI_BACKEND_DESCRIPTION@ URL: https://github.com/libusb/hidapi Version: @VERSION@ Libs: -L${libdir} -l@HIDAPI_BACKEND_LIBRARY_NAME@ +Libs.private: @HIDAPI_BACKEND_PRIVATE_LIBS@ Cflags: -I${includedir}/hidapi diff --git a/pc/hidapi.pc.in b/pc/hidapi.pc.in index f9aaf82c1..bf7d681ee 100644 --- a/pc/hidapi.pc.in +++ b/pc/hidapi.pc.in @@ -8,4 +8,5 @@ Description: C Library for USB/Bluetooth HID device access from Linux, Mac OS X, URL: https://github.com/libusb/hidapi Version: @VERSION@ Libs: -L${libdir} -l@HIDAPI_LIB_NAME@ +Libs.private: @HIDAPI_PRIVATE_LIBS@ Cflags: -I${includedir}/hidapi diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fc804fd54..d1845c5b2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -55,7 +55,7 @@ endif() # Helper(s) function(hidapi_configure_pc PC_IN_FILE) - set(oneValueArgs OUTPUT_NAME LIBRARY_NAME DESCRIPTION) + set(oneValueArgs OUTPUT_NAME LIBRARY_NAME DESCRIPTION PRIVATE_LIBS) cmake_parse_arguments(HIDAPI_PC "" "${oneValueArgs}" "" ${ARGN}) file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/pc") @@ -86,6 +86,8 @@ function(hidapi_configure_pc PC_IN_FILE) set(HIDAPI_BACKEND_NAME "${PC_IN_FILENAME}") set(HIDAPI_BACKEND_DESCRIPTION "${HIDAPI_PC_DESCRIPTION}") set(HIDAPI_BACKEND_LIBRARY_NAME "${HIDAPI_LIB_NAME}") + set(HIDAPI_BACKEND_PRIVATE_LIBS "${HIDAPI_PC_PRIVATE_LIBS}") + set(HIDAPI_PRIVATE_LIBS "${HIDAPI_PC_PRIVATE_LIBS}") set(PC_FILE "${CMAKE_CURRENT_BINARY_DIR}/pc/${PC_IN_FILENAME}.pc") configure_file("${PC_IN_FILE}" "${PC_FILE}" @ONLY) if(HIDAPI_INSTALL_TARGETS) @@ -124,7 +126,6 @@ set(EXPORT_COMPONENTS) set(HIDAPI_NEED_EXPORT_THREADS FALSE) set(HIDAPI_NEED_EXPORT_LIBUSB FALSE) -set(HIDAPI_NEED_EXPORT_LIBUDEV FALSE) set(HIDAPI_NEED_EXPORT_ICONV FALSE) if(WIN32) @@ -158,7 +159,6 @@ else() set(EXPORT_ALIAS hidraw) if(NOT BUILD_SHARED_LIBS) set(HIDAPI_NEED_EXPORT_THREADS TRUE) - set(HIDAPI_NEED_EXPORT_LIBUDEV TRUE) endif() endif() elseif(CMAKE_SYSTEM_NAME MATCHES "NetBSD") @@ -202,8 +202,13 @@ endif() add_library(hidapi::hidapi ALIAS hidapi_${EXPORT_ALIAS}) get_target_property(HIDAPI_LIB_NAME hidapi_${EXPORT_ALIAS} OUTPUT_NAME) +set(HIDAPI_GENERIC_PRIVATE_LIBS) +if(EXPORT_ALIAS STREQUAL "hidraw") + set(HIDAPI_GENERIC_PRIVATE_LIBS "-ldl") +endif() hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi.pc.in" LIBRARY_NAME "${HIDAPI_LIB_NAME}" + PRIVATE_LIBS "${HIDAPI_GENERIC_PRIVATE_LIBS}" ) if(HIDAPI_INSTALL_TARGETS) diff --git a/src/cmake/hidapi-config.cmake.in b/src/cmake/hidapi-config.cmake.in index 4226c8cd8..8888486bb 100644 --- a/src/cmake/hidapi-config.cmake.in +++ b/src/cmake/hidapi-config.cmake.in @@ -10,7 +10,6 @@ set(hidapi_FOUND FALSE) set(HIDAPI_NEED_EXPORT_THREADS @HIDAPI_NEED_EXPORT_THREADS@) set(HIDAPI_NEED_EXPORT_LIBUSB @HIDAPI_NEED_EXPORT_LIBUSB@) -set(HIDAPI_NEED_EXPORT_LIBUDEV @HIDAPI_NEED_EXPORT_LIBUDEV@) set(HIDAPI_NEED_EXPORT_ICONV @HIDAPI_NEED_EXPORT_ICONV@) if(HIDAPI_NEED_EXPORT_THREADS) @@ -20,17 +19,12 @@ if(HIDAPI_NEED_EXPORT_THREADS) find_package(Threads REQUIRED) endif() -if(HIDAPI_NEED_EXPORT_LIBUSB OR HIDAPI_NEED_EXPORT_LIBUDEV) +if(HIDAPI_NEED_EXPORT_LIBUSB) if(CMAKE_VERSION VERSION_LESS 3.6.3) message(FATAL_ERROR "This file relies on consumers using CMake 3.6.3 or greater.") endif() find_package(PkgConfig) - if(HIDAPI_NEED_EXPORT_LIBUSB) - pkg_check_modules(libusb REQUIRED IMPORTED_TARGET libusb-1.0>=1.0.9) - endif() - if(HIDAPI_NEED_EXPORT_LIBUDEV) - pkg_check_modules(libudev REQUIRED IMPORTED_TARGET libudev) - endif() + pkg_check_modules(libusb REQUIRED IMPORTED_TARGET libusb-1.0>=1.0.9) endif() if(HIDAPI_NEED_EXPORT_ICONV)