Skip to content

Commit

Permalink
RFC: add hid_get_report_descriptor API function (signal11#451)
Browse files Browse the repository at this point in the history
- native implementations for hidraw/libusb/macOS;
- HID Report Descriptor reconstruction from HIDP_PREPARSED_DATA on Windows;
- unit-tests for some known devices for `hid_winapi_descriptor_reconstruct_pp_data`;
- support for ASAN builds (mainly to check the `hid_winapi_descriptor_reconstruct_pp_data`/its unit-tests;
  • Loading branch information
Youw authored Mar 12, 2023
1 parent 88a0f02 commit 26b5bb0
Show file tree
Hide file tree
Showing 92 changed files with 22,107 additions and 20 deletions.
28 changes: 19 additions & 9 deletions .github/workflows/builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ jobs:
- name: Configure CMake
run: |
rm -rf build install
cmake -B build/shared -S hidapisrc -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}"
cmake -B build/static -S hidapisrc -DCMAKE_INSTALL_PREFIX=install/static -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}"
cmake -B build/framework -S hidapisrc -DCMAKE_INSTALL_PREFIX=install/framework -DCMAKE_FRAMEWORK=ON -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}"
cmake -B build/shared -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_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=${NIX_COMPILE_FLAGS}"
cmake -B build/framework -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/framework -DCMAKE_FRAMEWORK=ON -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}"
- name: Build CMake Shared
working-directory: build/shared
run: make install
Expand Down Expand Up @@ -110,8 +110,8 @@ jobs:
- name: Configure CMake
run: |
rm -rf build install
cmake -B build/shared -S hidapisrc -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}"
cmake -B build/static -S hidapisrc -DCMAKE_INSTALL_PREFIX=install/static -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}"
cmake -B build/shared -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_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=${NIX_COMPILE_FLAGS}"
- name: Build CMake Shared
working-directory: build/shared
run: make install
Expand Down Expand Up @@ -173,10 +173,10 @@ jobs:
- name: Configure CMake MSVC
shell: cmd
run: |
cmake -B build\msvc -S hidapisrc -DCMAKE_INSTALL_PREFIX=install\msvc -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=%MSVC_COMPILE_FLAGS%"
cmake -B build\msvc -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_PP_DATA_DUMP=ON -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install\msvc -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=%MSVC_COMPILE_FLAGS%"
- name: Build CMake MSVC
working-directory: build/msvc
run: cmake --build . --target install
run: cmake --build . --config RelWithDebInfo --target install
- name: Check artifacts MSVC
uses: andstor/file-existence-action@v2
with:
Expand All @@ -196,12 +196,16 @@ jobs:
"-DCMAKE_C_FLAGS=%MSVC_COMPILE_FLAGS%"
cd build\msvc_test
cmake --build . --target install
- name: Run CTest MSVC
shell: cmd
working-directory: build/msvc
run: ctest -C RelWithDebInfo --rerun-failed --output-on-failure

- name: Configure CMake NMake
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
cmake -G"NMake Makefiles" -B build\nmake -S hidapisrc -DCMAKE_INSTALL_PREFIX=install\nmake -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=%MSVC_COMPILE_FLAGS%"
cmake -G"NMake Makefiles" -B build\nmake -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_PP_DATA_DUMP=ON -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install\nmake -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=%MSVC_COMPILE_FLAGS%"
- name: Build CMake NMake
working-directory: build\nmake
shell: cmd
Expand Down Expand Up @@ -229,11 +233,14 @@ jobs:
"-DCMAKE_C_FLAGS=%MSVC_COMPILE_FLAGS%"
cd build\nmake_test
nmake install
- name: Run CTest NMake
working-directory: build\nmake
run: ctest --rerun-failed --output-on-failure

- name: Configure CMake MinGW
shell: cmd
run: |
cmake -G"MinGW Makefiles" -B build\mingw -S hidapisrc -DCMAKE_INSTALL_PREFIX=install\mingw -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=%NIX_COMPILE_FLAGS%"
cmake -G"MinGW Makefiles" -B build\mingw -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_PP_DATA_DUMP=ON -DCMAKE_INSTALL_PREFIX=install\mingw -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=%NIX_COMPILE_FLAGS%"
- name: Build CMake MinGW
working-directory: build\mingw
run: cmake --build . --target install
Expand All @@ -257,6 +264,9 @@ jobs:
"-DCMAKE_C_FLAGS=%NIX_COMPILE_FLAGS%"
cd build\mingw_test
cmake --build . --target install
- name: Run CTest MinGW
working-directory: build\mingw
run: ctest --rerun-failed --output-on-failure

- name: Check Meson build
shell: cmd
Expand Down
2 changes: 2 additions & 0 deletions BUILD.cmake.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ Some of the [standard](https://cmake.org/cmake/help/latest/manual/cmake-variable
HIDAPI-specific CMake variables:

- `HIDAPI_BUILD_HIDTEST` - when set to TRUE, build a small test application `hidtest`;
- `HIDAPI_WITH_TESTS` - when set to TRUE, build all (unit-)tests;
currently this option is only available on Windows, since only Windows backend has tests;

<details>
<summary>Linux-specific variables</summary>
Expand Down
54 changes: 49 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,57 @@ option(BUILD_SHARED_LIBS "Build shared version of the libraries, otherwise build
set(HIDAPI_INSTALL_TARGETS ON)
set(HIDAPI_PRINT_VERSION ON)

add_subdirectory(src)

set(BUILD_HIDTEST_DEFAULT OFF)
set(IS_DEBUG_BUILD OFF)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(BUILD_HIDTEST_DEFAULT ON)
set(IS_DEBUG_BUILD ON)
endif()

option(HIDAPI_ENABLE_ASAN "Build HIDAPI with ASAN address sanitizer instrumentation" OFF)

if(HIDAPI_ENABLE_ASAN)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address")
if(MSVC)
# the default is to have "/INCREMENTAL" which causes a warning when "-fsanitize=address" is present
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} /INCREMENTAL:NO")
set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG} /INCREMENTAL:NO")
set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} /INCREMENTAL:NO")
set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO} /INCREMENTAL:NO")
endif()
endif()

if(WIN32)
# so far only Windows has tests
option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" ${IS_DEBUG_BUILD})
else()
set(HIDAPI_WITH_TESTS OFF)
endif()

if(HIDAPI_WITH_TESTS)
enable_testing()
endif()

if(WIN32)
option(HIDAPI_BUILD_PP_DATA_DUMP "Build small Windows console application pp_data_dump.exe" ${IS_DEBUG_BUILD})
endif()
option(HIDAPI_BUILD_HIDTEST "Build small console test application hidtest" ${BUILD_HIDTEST_DEFAULT})

add_subdirectory(src)

option(HIDAPI_BUILD_HIDTEST "Build small console test application hidtest" ${IS_DEBUG_BUILD})
if(HIDAPI_BUILD_HIDTEST)
add_subdirectory(hidtest)
endif()

if(HIDAPI_ENABLE_ASAN)
if(NOT MSVC)
# MSVC doesn't recognize those options, other compilers - requiring it
foreach(HIDAPI_TARGET hidapi_winapi hidapi_darwin hidapi_hidraw hidapi_libusb hidtest_hidraw hidtest_libusb hidtest)
if(TARGET ${HIDAPI_TARGET})
if(BUILD_SHARED_LIBS)
target_link_options(${HIDAPI_TARGET} PRIVATE -fsanitize=address)
else()
target_link_options(${HIDAPI_TARGET} PUBLIC -fsanitize=address)
endif()
endif()
endforeach()
endif()
endif()
15 changes: 15 additions & 0 deletions hidapi/hidapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,21 @@ extern "C" {
*/
int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen);

/** @brief Get a report descriptor from a HID device.
User has to provide a preallocated buffer where descriptor will be copied to.
The recommended size for preallocated buffer is @ref HID_API_MAX_REPORT_DESCRIPTOR_SIZE bytes.
@ingroup API
@param dev A device handle returned from hid_open().
@param buf The buffer to copy descriptor into.
@param buf_size The size of the buffer in bytes.
@returns
This function returns non-negative number of bytes actually copied, or -1 on error.
*/
int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size);

/** @brief Get a string describing the last error which occurred.
This function is intended for logging/debugging purposes.
Expand Down
49 changes: 44 additions & 5 deletions hidtest/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,48 @@ void print_device(struct hid_device_info *cur_dev) {
printf("\n");
}

void print_hid_report_descriptor_from_device(hid_device *device) {
unsigned char descriptor[HID_API_MAX_REPORT_DESCRIPTOR_SIZE];
int res = 0;

printf(" Report Descriptor: ");
res = hid_get_report_descriptor(device, descriptor, sizeof(descriptor));
if (res < 0) {
printf("error getting: %ls", hid_error(device));
}
else {
printf("(%d bytes)", res);
}
for (int i = 0; i < res; i++) {
if (i % 10 == 0) {
printf("\n");
}
printf("0x%02x, ", descriptor[i]);
}
printf("\n");
}

void print_hid_report_descriptor_from_path(const char *path) {
hid_device *device = hid_open_path(path);
if (device) {
print_hid_report_descriptor_from_device(device);
hid_close(device);
}
else {
printf(" Report Descriptor: Unable to open device by path\n");
}
}

void print_devices(struct hid_device_info *cur_dev) {
while (cur_dev) {
for (; cur_dev; cur_dev = cur_dev->next) {
print_device(cur_dev);
}
}

void print_devices_with_descriptor(struct hid_device_info *cur_dev) {
for (; cur_dev; cur_dev = cur_dev->next) {
print_device(cur_dev);
cur_dev = cur_dev->next;
print_hid_report_descriptor_from_path(cur_dev->path);
}
}

Expand Down Expand Up @@ -103,7 +141,7 @@ int main(int argc, char* argv[])
#endif

devs = hid_enumerate(0x0, 0x0);
print_devices(devs);
print_devices_with_descriptor(devs);
hid_free_enumeration(devs);

// Set up the command buffer.
Expand Down Expand Up @@ -141,8 +179,9 @@ int main(int argc, char* argv[])
res = hid_get_serial_number_string(handle, wstr, MAX_STR);
if (res < 0)
printf("Unable to read serial number string\n");
printf("Serial Number String: (%d) %ls", wstr[0], wstr);
printf("\n");
printf("Serial Number String: (%d) %ls\n", wstr[0], wstr);

print_hid_report_descriptor_from_device(handle);

struct hid_device_info* info = hid_get_device_info(handle);
if (info == NULL) {
Expand Down
6 changes: 6 additions & 0 deletions libusb/hid.c
Original file line number Diff line number Diff line change
Expand Up @@ -1631,6 +1631,12 @@ int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index
}


int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size)
{
return hid_get_report_descriptor_libusb(dev->device_handle, dev->interface, dev->report_descriptor_size, buf, buf_size);
}


HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev)
{
(void)dev;
Expand Down
41 changes: 41 additions & 0 deletions linux/hid.c
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,28 @@ static int parse_hid_vid_pid_from_sysfs(const char *sysfs_path, unsigned *bus_ty
return res;
}

static int get_hid_report_descriptor_from_hidraw(hid_device *dev, struct hidraw_report_descriptor *rpt_desc)
{
int desc_size = 0;

/* Get Report Descriptor Size */
int res = ioctl(dev->device_handle, HIDIOCGRDESCSIZE, &desc_size);
if (res < 0) {
register_device_error_format(dev, "ioctl(GRDESCSIZE): %s", strerror(errno));
return res;
}

/* Get Report Descriptor */
memset(rpt_desc, 0x0, sizeof(*rpt_desc));
rpt_desc->size = desc_size;
res = ioctl(dev->device_handle, HIDIOCGRDESC, rpt_desc);
if (res < 0) {
register_device_error_format(dev, "ioctl(GRDESC): %s", strerror(errno));
}

return res;
}

/*
* The caller is responsible for free()ing the (newly-allocated) character
* strings pointed to by serial_number_utf8 and product_name_utf8 after use.
Expand Down Expand Up @@ -1236,6 +1258,25 @@ int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index
}


int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size)
{
struct hidraw_report_descriptor rpt_desc;
int res = get_hid_report_descriptor_from_hidraw(dev, &rpt_desc);
if (res < 0) {
/* error already registered */
return res;
}

if (rpt_desc.size < buf_size) {
buf_size = (size_t) rpt_desc.size;
}

memcpy(buf, rpt_desc.value, buf_size);

return (int) buf_size;
}


/* Passing in NULL means asking for the last global error message. */
HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev)
{
Expand Down
27 changes: 27 additions & 0 deletions mac/hid.c
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,33 @@ int HID_API_EXPORT_CALL hid_darwin_is_device_open_exclusive(hid_device *dev)
return (dev->open_options == kIOHIDOptionsTypeSeizeDevice) ? 1 : 0;
}

int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size)
{
CFTypeRef ref = IOHIDDeviceGetProperty(dev->device_handle, CFSTR(kIOHIDReportDescriptorKey));
if (ref != NULL && CFGetTypeID(ref) == CFDataGetTypeID()) {
CFDataRef report_descriptor = (CFDataRef) ref;
const UInt8 *descriptor_buf = CFDataGetBytePtr(report_descriptor);
CFIndex descriptor_buf_len = CFDataGetLength(report_descriptor);
size_t copy_len = (size_t) descriptor_buf_len;

if (descriptor_buf == NULL || descriptor_buf_len < 0) {
register_device_error(dev, "Zero buffer/length");
return -1;
}

if (buf_size < copy_len) {
copy_len = buf_size;
}

memcpy(buf, descriptor_buf, copy_len);
return copy_len;
}
else {
register_device_error(dev, "Failed to get kIOHIDReportDescriptorKey property");
return -1;
}
}

HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev)
{
if (dev) {
Expand Down
3 changes: 3 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ project(hidapi VERSION "${VERSION}" LANGUAGES C)

# Defaults and required options

if(NOT DEFINED HIDAPI_WITH_TESTS)
set(HIDAPI_WITH_TESTS OFF)
endif()
if(NOT DEFINED BUILD_SHARED_LIBS)
set(BUILD_SHARED_LIBS ON)
endif()
Expand Down
10 changes: 10 additions & 0 deletions windows/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ list(APPEND HIDAPI_PUBLIC_HEADERS "hidapi_winapi.h")
set(SOURCES
hid.c
hidapi_cfgmgr32.h
hidapi_descriptor_reconstruct.c
hidapi_descriptor_reconstruct.h
hidapi_hidclass.h
hidapi_hidpi.h
hidapi_hidsdi.h
Expand Down Expand Up @@ -51,3 +53,11 @@ if(HIDAPI_INSTALL_TARGETS)
endif()

hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi.pc.in")

if(HIDAPI_WITH_TESTS)
add_subdirectory(test)
endif()

if(DEFINED HIDAPI_BUILD_PP_DATA_DUMP AND HIDAPI_BUILD_PP_DATA_DUMP)
add_subdirectory(pp_data_dump)
endif()
Loading

0 comments on commit 26b5bb0

Please sign in to comment.