Skip to content

Commit 26b5bb0

Browse files
authored
RFC: add hid_get_report_descriptor API function (#451)
- 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;
1 parent 88a0f02 commit 26b5bb0

92 files changed

Lines changed: 22107 additions & 20 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/builds.yml

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ jobs:
4242
- name: Configure CMake
4343
run: |
4444
rm -rf build install
45-
cmake -B build/shared -S hidapisrc -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}"
46-
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}"
47-
cmake -B build/framework -S hidapisrc -DCMAKE_INSTALL_PREFIX=install/framework -DCMAKE_FRAMEWORK=ON -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}"
45+
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}"
46+
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}"
47+
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}"
4848
- name: Build CMake Shared
4949
working-directory: build/shared
5050
run: make install
@@ -110,8 +110,8 @@ jobs:
110110
- name: Configure CMake
111111
run: |
112112
rm -rf build install
113-
cmake -B build/shared -S hidapisrc -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}"
114-
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}"
113+
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}"
114+
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}"
115115
- name: Build CMake Shared
116116
working-directory: build/shared
117117
run: make install
@@ -173,10 +173,10 @@ jobs:
173173
- name: Configure CMake MSVC
174174
shell: cmd
175175
run: |
176-
cmake -B build\msvc -S hidapisrc -DCMAKE_INSTALL_PREFIX=install\msvc -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=%MSVC_COMPILE_FLAGS%"
176+
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%"
177177
- name: Build CMake MSVC
178178
working-directory: build/msvc
179-
run: cmake --build . --target install
179+
run: cmake --build . --config RelWithDebInfo --target install
180180
- name: Check artifacts MSVC
181181
uses: andstor/file-existence-action@v2
182182
with:
@@ -196,12 +196,16 @@ jobs:
196196
"-DCMAKE_C_FLAGS=%MSVC_COMPILE_FLAGS%"
197197
cd build\msvc_test
198198
cmake --build . --target install
199+
- name: Run CTest MSVC
200+
shell: cmd
201+
working-directory: build/msvc
202+
run: ctest -C RelWithDebInfo --rerun-failed --output-on-failure
199203

200204
- name: Configure CMake NMake
201205
shell: cmd
202206
run: |
203207
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
204-
cmake -G"NMake Makefiles" -B build\nmake -S hidapisrc -DCMAKE_INSTALL_PREFIX=install\nmake -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=%MSVC_COMPILE_FLAGS%"
208+
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%"
205209
- name: Build CMake NMake
206210
working-directory: build\nmake
207211
shell: cmd
@@ -229,11 +233,14 @@ jobs:
229233
"-DCMAKE_C_FLAGS=%MSVC_COMPILE_FLAGS%"
230234
cd build\nmake_test
231235
nmake install
236+
- name: Run CTest NMake
237+
working-directory: build\nmake
238+
run: ctest --rerun-failed --output-on-failure
232239

233240
- name: Configure CMake MinGW
234241
shell: cmd
235242
run: |
236-
cmake -G"MinGW Makefiles" -B build\mingw -S hidapisrc -DCMAKE_INSTALL_PREFIX=install\mingw -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=%NIX_COMPILE_FLAGS%"
243+
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%"
237244
- name: Build CMake MinGW
238245
working-directory: build\mingw
239246
run: cmake --build . --target install
@@ -257,6 +264,9 @@ jobs:
257264
"-DCMAKE_C_FLAGS=%NIX_COMPILE_FLAGS%"
258265
cd build\mingw_test
259266
cmake --build . --target install
267+
- name: Run CTest MinGW
268+
working-directory: build\mingw
269+
run: ctest --rerun-failed --output-on-failure
260270

261271
- name: Check Meson build
262272
shell: cmd

BUILD.cmake.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ Some of the [standard](https://cmake.org/cmake/help/latest/manual/cmake-variable
8888
HIDAPI-specific CMake variables:
8989

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

9294
<details>
9395
<summary>Linux-specific variables</summary>

CMakeLists.txt

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,57 @@ option(BUILD_SHARED_LIBS "Build shared version of the libraries, otherwise build
4949
set(HIDAPI_INSTALL_TARGETS ON)
5050
set(HIDAPI_PRINT_VERSION ON)
5151

52-
add_subdirectory(src)
53-
54-
set(BUILD_HIDTEST_DEFAULT OFF)
52+
set(IS_DEBUG_BUILD OFF)
5553
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
56-
set(BUILD_HIDTEST_DEFAULT ON)
54+
set(IS_DEBUG_BUILD ON)
55+
endif()
56+
57+
option(HIDAPI_ENABLE_ASAN "Build HIDAPI with ASAN address sanitizer instrumentation" OFF)
58+
59+
if(HIDAPI_ENABLE_ASAN)
60+
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address")
61+
if(MSVC)
62+
# the default is to have "/INCREMENTAL" which causes a warning when "-fsanitize=address" is present
63+
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} /INCREMENTAL:NO")
64+
set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG} /INCREMENTAL:NO")
65+
set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} /INCREMENTAL:NO")
66+
set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO} /INCREMENTAL:NO")
67+
endif()
68+
endif()
69+
70+
if(WIN32)
71+
# so far only Windows has tests
72+
option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" ${IS_DEBUG_BUILD})
73+
else()
74+
set(HIDAPI_WITH_TESTS OFF)
75+
endif()
76+
77+
if(HIDAPI_WITH_TESTS)
78+
enable_testing()
79+
endif()
80+
81+
if(WIN32)
82+
option(HIDAPI_BUILD_PP_DATA_DUMP "Build small Windows console application pp_data_dump.exe" ${IS_DEBUG_BUILD})
5783
endif()
58-
option(HIDAPI_BUILD_HIDTEST "Build small console test application hidtest" ${BUILD_HIDTEST_DEFAULT})
84+
85+
add_subdirectory(src)
86+
87+
option(HIDAPI_BUILD_HIDTEST "Build small console test application hidtest" ${IS_DEBUG_BUILD})
5988
if(HIDAPI_BUILD_HIDTEST)
6089
add_subdirectory(hidtest)
6190
endif()
91+
92+
if(HIDAPI_ENABLE_ASAN)
93+
if(NOT MSVC)
94+
# MSVC doesn't recognize those options, other compilers - requiring it
95+
foreach(HIDAPI_TARGET hidapi_winapi hidapi_darwin hidapi_hidraw hidapi_libusb hidtest_hidraw hidtest_libusb hidtest)
96+
if(TARGET ${HIDAPI_TARGET})
97+
if(BUILD_SHARED_LIBS)
98+
target_link_options(${HIDAPI_TARGET} PRIVATE -fsanitize=address)
99+
else()
100+
target_link_options(${HIDAPI_TARGET} PUBLIC -fsanitize=address)
101+
endif()
102+
endif()
103+
endforeach()
104+
endif()
105+
endif()

hidapi/hidapi.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,21 @@ extern "C" {
550550
*/
551551
int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen);
552552

553+
/** @brief Get a report descriptor from a HID device.
554+
555+
User has to provide a preallocated buffer where descriptor will be copied to.
556+
The recommended size for preallocated buffer is @ref HID_API_MAX_REPORT_DESCRIPTOR_SIZE bytes.
557+
558+
@ingroup API
559+
@param dev A device handle returned from hid_open().
560+
@param buf The buffer to copy descriptor into.
561+
@param buf_size The size of the buffer in bytes.
562+
563+
@returns
564+
This function returns non-negative number of bytes actually copied, or -1 on error.
565+
*/
566+
int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size);
567+
553568
/** @brief Get a string describing the last error which occurred.
554569
555570
This function is intended for logging/debugging purposes.

hidtest/test.c

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,48 @@ void print_device(struct hid_device_info *cur_dev) {
6464
printf("\n");
6565
}
6666

67+
void print_hid_report_descriptor_from_device(hid_device *device) {
68+
unsigned char descriptor[HID_API_MAX_REPORT_DESCRIPTOR_SIZE];
69+
int res = 0;
70+
71+
printf(" Report Descriptor: ");
72+
res = hid_get_report_descriptor(device, descriptor, sizeof(descriptor));
73+
if (res < 0) {
74+
printf("error getting: %ls", hid_error(device));
75+
}
76+
else {
77+
printf("(%d bytes)", res);
78+
}
79+
for (int i = 0; i < res; i++) {
80+
if (i % 10 == 0) {
81+
printf("\n");
82+
}
83+
printf("0x%02x, ", descriptor[i]);
84+
}
85+
printf("\n");
86+
}
87+
88+
void print_hid_report_descriptor_from_path(const char *path) {
89+
hid_device *device = hid_open_path(path);
90+
if (device) {
91+
print_hid_report_descriptor_from_device(device);
92+
hid_close(device);
93+
}
94+
else {
95+
printf(" Report Descriptor: Unable to open device by path\n");
96+
}
97+
}
98+
6799
void print_devices(struct hid_device_info *cur_dev) {
68-
while (cur_dev) {
100+
for (; cur_dev; cur_dev = cur_dev->next) {
101+
print_device(cur_dev);
102+
}
103+
}
104+
105+
void print_devices_with_descriptor(struct hid_device_info *cur_dev) {
106+
for (; cur_dev; cur_dev = cur_dev->next) {
69107
print_device(cur_dev);
70-
cur_dev = cur_dev->next;
108+
print_hid_report_descriptor_from_path(cur_dev->path);
71109
}
72110
}
73111

@@ -103,7 +141,7 @@ int main(int argc, char* argv[])
103141
#endif
104142

105143
devs = hid_enumerate(0x0, 0x0);
106-
print_devices(devs);
144+
print_devices_with_descriptor(devs);
107145
hid_free_enumeration(devs);
108146

109147
// Set up the command buffer.
@@ -141,8 +179,9 @@ int main(int argc, char* argv[])
141179
res = hid_get_serial_number_string(handle, wstr, MAX_STR);
142180
if (res < 0)
143181
printf("Unable to read serial number string\n");
144-
printf("Serial Number String: (%d) %ls", wstr[0], wstr);
145-
printf("\n");
182+
printf("Serial Number String: (%d) %ls\n", wstr[0], wstr);
183+
184+
print_hid_report_descriptor_from_device(handle);
146185

147186
struct hid_device_info* info = hid_get_device_info(handle);
148187
if (info == NULL) {

libusb/hid.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,6 +1631,12 @@ int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index
16311631
}
16321632

16331633

1634+
int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size)
1635+
{
1636+
return hid_get_report_descriptor_libusb(dev->device_handle, dev->interface, dev->report_descriptor_size, buf, buf_size);
1637+
}
1638+
1639+
16341640
HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev)
16351641
{
16361642
(void)dev;

linux/hid.c

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,28 @@ static int parse_hid_vid_pid_from_sysfs(const char *sysfs_path, unsigned *bus_ty
481481
return res;
482482
}
483483

484+
static int get_hid_report_descriptor_from_hidraw(hid_device *dev, struct hidraw_report_descriptor *rpt_desc)
485+
{
486+
int desc_size = 0;
487+
488+
/* Get Report Descriptor Size */
489+
int res = ioctl(dev->device_handle, HIDIOCGRDESCSIZE, &desc_size);
490+
if (res < 0) {
491+
register_device_error_format(dev, "ioctl(GRDESCSIZE): %s", strerror(errno));
492+
return res;
493+
}
494+
495+
/* Get Report Descriptor */
496+
memset(rpt_desc, 0x0, sizeof(*rpt_desc));
497+
rpt_desc->size = desc_size;
498+
res = ioctl(dev->device_handle, HIDIOCGRDESC, rpt_desc);
499+
if (res < 0) {
500+
register_device_error_format(dev, "ioctl(GRDESC): %s", strerror(errno));
501+
}
502+
503+
return res;
504+
}
505+
484506
/*
485507
* The caller is responsible for free()ing the (newly-allocated) character
486508
* strings pointed to by serial_number_utf8 and product_name_utf8 after use.
@@ -1236,6 +1258,25 @@ int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index
12361258
}
12371259

12381260

1261+
int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size)
1262+
{
1263+
struct hidraw_report_descriptor rpt_desc;
1264+
int res = get_hid_report_descriptor_from_hidraw(dev, &rpt_desc);
1265+
if (res < 0) {
1266+
/* error already registered */
1267+
return res;
1268+
}
1269+
1270+
if (rpt_desc.size < buf_size) {
1271+
buf_size = (size_t) rpt_desc.size;
1272+
}
1273+
1274+
memcpy(buf, rpt_desc.value, buf_size);
1275+
1276+
return (int) buf_size;
1277+
}
1278+
1279+
12391280
/* Passing in NULL means asking for the last global error message. */
12401281
HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev)
12411282
{

mac/hid.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,33 @@ int HID_API_EXPORT_CALL hid_darwin_is_device_open_exclusive(hid_device *dev)
14311431
return (dev->open_options == kIOHIDOptionsTypeSeizeDevice) ? 1 : 0;
14321432
}
14331433

1434+
int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size)
1435+
{
1436+
CFTypeRef ref = IOHIDDeviceGetProperty(dev->device_handle, CFSTR(kIOHIDReportDescriptorKey));
1437+
if (ref != NULL && CFGetTypeID(ref) == CFDataGetTypeID()) {
1438+
CFDataRef report_descriptor = (CFDataRef) ref;
1439+
const UInt8 *descriptor_buf = CFDataGetBytePtr(report_descriptor);
1440+
CFIndex descriptor_buf_len = CFDataGetLength(report_descriptor);
1441+
size_t copy_len = (size_t) descriptor_buf_len;
1442+
1443+
if (descriptor_buf == NULL || descriptor_buf_len < 0) {
1444+
register_device_error(dev, "Zero buffer/length");
1445+
return -1;
1446+
}
1447+
1448+
if (buf_size < copy_len) {
1449+
copy_len = buf_size;
1450+
}
1451+
1452+
memcpy(buf, descriptor_buf, copy_len);
1453+
return copy_len;
1454+
}
1455+
else {
1456+
register_device_error(dev, "Failed to get kIOHIDReportDescriptorKey property");
1457+
return -1;
1458+
}
1459+
}
1460+
14341461
HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev)
14351462
{
14361463
if (dev) {

src/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ project(hidapi VERSION "${VERSION}" LANGUAGES C)
2626

2727
# Defaults and required options
2828

29+
if(NOT DEFINED HIDAPI_WITH_TESTS)
30+
set(HIDAPI_WITH_TESTS OFF)
31+
endif()
2932
if(NOT DEFINED BUILD_SHARED_LIBS)
3033
set(BUILD_SHARED_LIBS ON)
3134
endif()

windows/CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ list(APPEND HIDAPI_PUBLIC_HEADERS "hidapi_winapi.h")
33
set(SOURCES
44
hid.c
55
hidapi_cfgmgr32.h
6+
hidapi_descriptor_reconstruct.c
7+
hidapi_descriptor_reconstruct.h
68
hidapi_hidclass.h
79
hidapi_hidpi.h
810
hidapi_hidsdi.h
@@ -51,3 +53,11 @@ if(HIDAPI_INSTALL_TARGETS)
5153
endif()
5254

5355
hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi.pc.in")
56+
57+
if(HIDAPI_WITH_TESTS)
58+
add_subdirectory(test)
59+
endif()
60+
61+
if(DEFINED HIDAPI_BUILD_PP_DATA_DUMP AND HIDAPI_BUILD_PP_DATA_DUMP)
62+
add_subdirectory(pp_data_dump)
63+
endif()

0 commit comments

Comments
 (0)