diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5021bbe952..e1b43842a3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -50,7 +50,6 @@ build-retroarch-windows-x64: --enforcedir /usr/lib/mxe/usr/x86_64-w64-mingw32.shared/qt5/plugins/styles/ --copy --objdump x86_64-w64-mingw32.shared-objdump" - - "cp -f nvdaControllerClient64.dll ${MEDIA_PATH}/${CI_PROJECT_NAME}/redist/nvdaControllerClient64.dll" - "rm -f ${MEDIA_PATH}/${CI_PROJECT_NAME}/redist/opengl32.dll" build-retroarch-windows-i686: diff --git a/Makefile.common b/Makefile.common index 319967cb5e..4d09bd39ab 100644 --- a/Makefile.common +++ b/Makefile.common @@ -2939,6 +2939,7 @@ ifneq ($(findstring Win32,$(OS)),) OBJ += media/rarch.o \ gfx/common/win32_common.o \ frontend/drivers/platform_win32.o \ + frontend/drivers/nvda_controller_win32.o \ gfx/display_servers/dispserv_win32.o \ gfx/common/dx_guids.o diff --git a/frontend/drivers/nvda_controller_win32.c b/frontend/drivers/nvda_controller_win32.c new file mode 100644 index 0000000000..11283605c1 --- /dev/null +++ b/frontend/drivers/nvda_controller_win32.c @@ -0,0 +1,350 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +/* NVDA's controller interface over local RPC, as NVDA's controller + * client DLL speaks it. + * + * NVDA listens on ncalrpc:[NvdaCtlr..] for + * interface DFF50B99-F7FD-4CA7-A82C-DAEB3E025295 v1.0, whose four + * procedures - testIfRunning, speakText, cancelSpeech, brailleMessage - + * NVDA publishes in nvdaHelper/interfaces/nvdaController.idl. The + * calls go through NdrClientCall2 with the format strings MIDL makes + * for that interface: the x64 ones as NVDA's own client carries them, + * the x86 ones the same procedures for the x86 stack. Every procedure + * has comm and fault status, so a failed call - NVDA not running - + * comes back as its return value, never as an exception. + * + * rpcrt4 and the session and desktop calls are resolved at run time: + * where any is missing, as before Windows 2000, there is no NVDA to + * reach and nvda_controller_init() says so. */ + +#ifdef HAVE_CONFIG_H +#include "../../config.h" +#endif + +#ifdef HAVE_NVDA + +#include +#include + +#include +#include +#include + +#include "nvda_controller_win32.h" + +#if defined(_M_X64) || defined(__x86_64__) +#define NVDA_CONTROLLER_X64 +#elif defined(_M_IX86) || defined(__i386__) +#define NVDA_CONTROLLER_X86 +#endif + +#if defined(NVDA_CONTROLLER_X64) || defined(NVDA_CONTROLLER_X86) + +typedef CLIENT_CALL_RETURN (RPC_VAR_ENTRY *nvda_ndr_client_call2_t)( + PMIDL_STUB_DESC, PFORMAT_STRING, ...); +typedef RPC_STATUS (RPC_ENTRY *nvda_rpc_binding_from_string_t)( + unsigned short *, RPC_BINDING_HANDLE *); +typedef RPC_STATUS (RPC_ENTRY *nvda_rpc_binding_free_t)(RPC_BINDING_HANDLE *); +typedef BOOL (WINAPI *nvda_process_id_to_session_id_t)(DWORD, DWORD *); +typedef HDESK (WINAPI *nvda_get_thread_desktop_t)(DWORD); +typedef BOOL (WINAPI *nvda_get_user_object_information_w_t)(HANDLE, int, + PVOID, DWORD, LPDWORD); + +/* Procedure format strings, one procedure each: the procedure header, + * then its [in, string] wchar_t * where it has one, then its + * error_status_t return. Indexed by NVDA_PROC_*. */ +#define NVDA_PROC_TEST_IF_RUNNING 0 +#define NVDA_PROC_SPEAK_TEXT 1 +#define NVDA_PROC_CANCEL_SPEECH 2 +#define NVDA_PROC_BRAILLE_MESSAGE 3 + +#ifdef NVDA_CONTROLLER_X64 +static const unsigned char nvda_proc_format[] = { + /* 0: testIfRunning - implicit handle, comm/fault status, method 0, + * stack 8, no arguments; the extension's 10 bytes */ + 0x32, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x44, 0x01, 0x0a, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x10, 0x00, + /* 32: speakText - method 1, stack 16, the string at 0 */ + 0x32, 0x68, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x46, 0x02, 0x0a, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0b, 0x01, 0x00, 0x00, 0x04, 0x00, + 0x70, 0x00, 0x08, 0x00, 0x10, 0x00, + /* 70: cancelSpeech - method 2 */ + 0x32, 0x68, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x08, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x44, 0x01, 0x0a, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x10, 0x00, + /* 102: brailleMessage - method 3 */ + 0x32, 0x68, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x46, 0x02, 0x0a, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0b, 0x01, 0x00, 0x00, 0x04, 0x00, + 0x70, 0x00, 0x08, 0x00, 0x10, 0x00, + 0x00 +}; +static const unsigned short nvda_proc_offset[4] = { 0, 32, 70, 102 }; +#else +static const unsigned char nvda_proc_format[] = { + /* 0: testIfRunning - method 0, stack 4; the extension's 8 bytes */ + 0x32, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x44, 0x01, 0x08, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x10, 0x00, + /* 30: speakText - method 1, stack 8, the string at 0 */ + 0x32, 0x68, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x46, 0x02, 0x08, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x0b, 0x01, 0x00, 0x00, 0x04, 0x00, + 0x70, 0x00, 0x04, 0x00, 0x10, 0x00, + /* 66: cancelSpeech - method 2 */ + 0x32, 0x68, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x44, 0x01, 0x08, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x10, 0x00, + /* 96: brailleMessage - method 3 */ + 0x32, 0x68, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x08, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x46, 0x02, 0x08, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x0b, 0x01, 0x00, 0x00, 0x04, 0x00, + 0x70, 0x00, 0x04, 0x00, 0x10, 0x00, + 0x00 +}; +static const unsigned short nvda_proc_offset[4] = { 0, 30, 66, 96 }; +#endif + +/* The one type: at 2 a reference pointer to, at 4, a conformant + * wide string */ +static const unsigned char nvda_type_format[] = { + 0x00, 0x00, 0x11, 0x08, 0x25, 0x5c, 0x00 +}; + +/* Comm and fault status of each procedure in its return value */ +static const COMM_FAULT_OFFSETS nvda_comm_fault_offsets[4] = { + { -1, -1 }, { -1, -1 }, { -1, -1 }, { -1, -1 } +}; + +static const RPC_CLIENT_INTERFACE nvda_client_interface = { + sizeof(RPC_CLIENT_INTERFACE), + /* DFF50B99-F7FD-4CA7-A82C-DAEB3E025295 v1.0 */ + { { 0xdff50b99, 0xf7fd, 0x4ca7, + { 0xa8, 0x2c, 0xda, 0xeb, 0x3e, 0x02, 0x52, 0x95 } }, { 1, 0 } }, + /* NDR 2.0 */ + { { 0x8a885d04, 0x1ceb, 0x11c9, + { 0x9f, 0xe8, 0x08, 0x00, 0x2b, 0x10, 0x48, 0x60 } }, { 2, 0 } }, + 0, 0, 0, 0, 0, 0 +}; + +static handle_t nvda_binding; + +static void * __RPC_USER nvda_rpc_alloc(size_t len) +{ + return malloc(len); +} + +static void __RPC_USER nvda_rpc_free(void *ptr) +{ + free(ptr); +} + +static MIDL_STUB_DESC nvda_stub_desc; + +static HMODULE nvda_rpcrt4; +static nvda_ndr_client_call2_t nvda_ndr_client_call2; +static nvda_rpc_binding_free_t nvda_rpc_binding_free; + +static unsigned long nvda_call_void(unsigned proc) +{ + CLIENT_CALL_RETURN ret; + if (!nvda_binding) + return RPC_S_INVALID_BINDING; + ret = nvda_ndr_client_call2(&nvda_stub_desc, + (PFORMAT_STRING)&nvda_proc_format[nvda_proc_offset[proc]]); + return (unsigned long)ret.Simple; +} + +static unsigned long nvda_call_string(unsigned proc, const wchar_t *text) +{ + CLIENT_CALL_RETURN ret; + if (!nvda_binding) + return RPC_S_INVALID_BINDING; + if (!text) + return RPC_X_NULL_REF_POINTER; +#ifdef NVDA_CONTROLLER_X64 + ret = nvda_ndr_client_call2(&nvda_stub_desc, + (PFORMAT_STRING)&nvda_proc_format[nvda_proc_offset[proc]], text); +#else + { + /* x86: the interpreter copies the procedure's whole stack - the + * string, then the return value's slot - from where it is told + * the arguments start */ + const wchar_t *args[2]; + args[0] = text; + args[1] = NULL; + ret = nvda_ndr_client_call2(&nvda_stub_desc, + (PFORMAT_STRING)&nvda_proc_format[nvda_proc_offset[proc]], + args); + } +#endif + return (unsigned long)ret.Simple; +} + +/* @out of @len wide characters: ncalrpc:[NvdaCtlr..] */ +static bool nvda_binding_string(wchar_t *out, size_t len) +{ + static const wchar_t prefix[] = L"ncalrpc:[NvdaCtlr."; + wchar_t desktop[64]; + wchar_t digits[16]; + DWORD session = 0; + size_t n, i, d = 0; + HMODULE kernel32 = GetModuleHandleA("kernel32.dll"); + /* Loaded already where there is a window, but not everywhere */ + HMODULE user32 = LoadLibraryA("user32.dll"); + nvda_process_id_to_session_id_t session_of; + nvda_get_thread_desktop_t thread_desktop; + nvda_get_user_object_information_w_t object_info; + HDESK desk; + + if (!kernel32 || !user32) + return false; + session_of = (nvda_process_id_to_session_id_t)(void (*)(void)) + GetProcAddress(kernel32, "ProcessIdToSessionId"); + thread_desktop = (nvda_get_thread_desktop_t)(void (*)(void)) + GetProcAddress(user32, "GetThreadDesktop"); + object_info = (nvda_get_user_object_information_w_t)(void (*)(void)) + GetProcAddress(user32, "GetUserObjectInformationW"); + if (!session_of || !thread_desktop || !object_info) + return false; + if (!session_of(GetCurrentProcessId(), &session)) + return false; + if (!(desk = thread_desktop(GetCurrentThreadId()))) + return false; + memset(desktop, 0, sizeof(desktop)); + /* UOI_NAME */ + if (!object_info(desk, 2, desktop, sizeof(desktop) - sizeof(wchar_t), + NULL)) + return false; + + do + { + digits[d++] = (wchar_t)(L'0' + session % 10); + session /= 10; + } while (session); + + n = 0; + for (i = 0; prefix[i]; i++) + if (n < len) + out[n++] = prefix[i]; + while (d) + if (n < len) + out[n++] = digits[--d]; + else + d--; + if (n < len) + out[n++] = L'.'; + for (i = 0; desktop[i]; i++) + if (n < len) + out[n++] = desktop[i]; + if (n + 2 > len) + return false; + out[n++] = L']'; + out[n] = 0; + return true; +} + +bool nvda_controller_init(void) +{ + wchar_t binding[128]; + nvda_rpc_binding_from_string_t from_string; + + if (nvda_binding) + return true; + if (!nvda_binding_string(binding, sizeof(binding) / sizeof(binding[0]))) + return false; + if (!nvda_rpcrt4 && !(nvda_rpcrt4 = LoadLibraryA("rpcrt4.dll"))) + return false; + nvda_ndr_client_call2 = (nvda_ndr_client_call2_t)(void (*)(void)) + GetProcAddress(nvda_rpcrt4, "NdrClientCall2"); + nvda_rpc_binding_free = (nvda_rpc_binding_free_t)(void (*)(void)) + GetProcAddress(nvda_rpcrt4, "RpcBindingFree"); + from_string = (nvda_rpc_binding_from_string_t)(void (*)(void)) + GetProcAddress(nvda_rpcrt4, "RpcBindingFromStringBindingW"); + if ( !nvda_ndr_client_call2 + || !nvda_rpc_binding_free + || !from_string) + return false; + + nvda_stub_desc.RpcInterfaceInformation = (void*)&nvda_client_interface; + nvda_stub_desc.pfnAllocate = nvda_rpc_alloc; + nvda_stub_desc.pfnFree = nvda_rpc_free; + nvda_stub_desc.IMPLICIT_HANDLE_INFO.pPrimitiveHandle = &nvda_binding; + nvda_stub_desc.pFormatTypes = nvda_type_format; + nvda_stub_desc.fCheckBounds = 1; + nvda_stub_desc.Version = 0x50002; + nvda_stub_desc.MIDLVersion = 0x700022b; + nvda_stub_desc.CommFaultOffsets = nvda_comm_fault_offsets; + + if (from_string((unsigned short*)binding, &nvda_binding) != RPC_S_OK) + { + nvda_binding = NULL; + return false; + } + return true; +} + +void nvda_controller_deinit(void) +{ + if (nvda_binding) + nvda_rpc_binding_free(&nvda_binding); + nvda_binding = NULL; +} + +unsigned long nvda_controller_test_if_running(void) +{ + return nvda_call_void(NVDA_PROC_TEST_IF_RUNNING); +} + +unsigned long nvda_controller_cancel_speech(void) +{ + return nvda_call_void(NVDA_PROC_CANCEL_SPEECH); +} + +unsigned long nvda_controller_speak_text(const wchar_t *text) +{ + return nvda_call_string(NVDA_PROC_SPEAK_TEXT, text); +} + +unsigned long nvda_controller_braille_message(const wchar_t *message) +{ + return nvda_call_string(NVDA_PROC_BRAILLE_MESSAGE, message); +} + +#else + +/* No format strings for this architecture: no NVDA */ +bool nvda_controller_init(void) { return false; } +void nvda_controller_deinit(void) { } +unsigned long nvda_controller_test_if_running(void) { return 1; } +unsigned long nvda_controller_cancel_speech(void) { return 1; } +unsigned long nvda_controller_speak_text(const wchar_t *text) +{ (void)text; return 1; } +unsigned long nvda_controller_braille_message(const wchar_t *message) +{ (void)message; return 1; } + +#endif + +#endif /* HAVE_NVDA */ diff --git a/frontend/drivers/nvda_controller_win32.h b/frontend/drivers/nvda_controller_win32.h new file mode 100644 index 0000000000..6f08059f2a --- /dev/null +++ b/frontend/drivers/nvda_controller_win32.h @@ -0,0 +1,44 @@ +/* RetroArch - A frontend for libretro. + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#ifndef __RARCH_NVDA_CONTROLLER_WIN32_H +#define __RARCH_NVDA_CONTROLLER_WIN32_H + +#include + +#include +#include + +RETRO_BEGIN_DECLS + +/* The NVDA screen reader's controller interface, spoken to over local + * RPC as NVDA's own controller client does: no DLL to ship or load. + * + * nvda_controller_init() binds to the NVDA instance on this session + * and desktop; false when this Windows or this build has no way to + * reach one (the calls below then return nonzero). It binds once and + * holds the binding until nvda_controller_deinit(). Each call returns + * 0 on success and the RPC status otherwise - nonzero, notably, when + * NVDA is not running. */ +bool nvda_controller_init(void); +void nvda_controller_deinit(void); + +unsigned long nvda_controller_test_if_running(void); +unsigned long nvda_controller_cancel_speech(void); +unsigned long nvda_controller_speak_text(const wchar_t *text); +unsigned long nvda_controller_braille_message(const wchar_t *message); + +RETRO_END_DECLS + +#endif diff --git a/frontend/drivers/platform_win32.c b/frontend/drivers/platform_win32.c index f38addf4fb..8f28e960fd 100644 --- a/frontend/drivers/platform_win32.c +++ b/frontend/drivers/platform_win32.c @@ -58,6 +58,9 @@ #include "../../gfx/common/win32_common.h" #include "platform_win32.h" +#ifdef HAVE_NVDA +#include "nvda_controller_win32.h" +#endif #ifdef HAVE_THREADS #include "../thread_elevation.h" #endif @@ -110,14 +113,12 @@ static char win32_cpu_model_name[64] = {0}; * it early seems to cause issues on some systems. */ static dylib_t dwm_lib; -static dylib_t nvda_lib; #endif -/* Dynamic loading for Non-Visual Desktop Access support */ -unsigned long (__stdcall *nvdaController_testIfRunning_func)(void); -unsigned long (__stdcall *nvdaController_cancelSpeech_func)(void); -unsigned long (__stdcall *nvdaController_brailleMessage_func)(wchar_t*); -unsigned long (__stdcall *nvdaController_speakText_func)(wchar_t*); +#ifdef HAVE_NVDA +/* Bound to NVDA's controller endpoint */ +static bool nvda_bound; +#endif #if defined(HAVE_LANGEXTRA) && !defined(_XBOX) #if (defined(_WIN32_WINNT) && _WIN32_WINNT >= 0x0500) || !defined(_MSC_VER) @@ -1141,30 +1142,16 @@ static void frontend_win32_init(void *data) #ifdef HAVE_NVDA +/* Runs on each accessibility speak event: binds once, and where there + * is no NVDA to bind to, PowerShell speaks instead */ static void init_nvda(void) { -#ifdef HAVE_DYLIB - if ( (g_plat_win32_flags & PLAT_WIN32_FLAG_USE_NVDA) - && !nvda_lib) - { - if ((nvda_lib = dylib_load("nvdaControllerClient64.dll"))) - { - nvdaController_testIfRunning_func = (unsigned long (__stdcall*)(void))dylib_proc(nvda_lib, "nvdaController_testIfRunning"); - nvdaController_cancelSpeech_func = (unsigned long(__stdcall *)(void))dylib_proc(nvda_lib, "nvdaController_cancelSpeech"); - nvdaController_brailleMessage_func = (unsigned long(__stdcall *)(wchar_t*))dylib_proc(nvda_lib, "nvdaController_brailleMessage"); - nvdaController_speakText_func = (unsigned long(__stdcall *)(wchar_t*))dylib_proc(nvda_lib, "nvdaController_speakText"); - return; - } - } -#endif - /* The above code is executed on each accessibility speak event, so - * we should only revert to powershell if nvda_lib wasn't loaded previously, - * and we weren't able to load it on this call, or we don't HAVE_DYLIB */ - if ((g_plat_win32_flags & PLAT_WIN32_FLAG_USE_NVDA) && !nvda_lib) - { - g_plat_win32_flags &= ~PLAT_WIN32_FLAG_USE_NVDA; - g_plat_win32_flags |= PLAT_WIN32_FLAG_USE_POWERSHELL; - } + if (!(g_plat_win32_flags & PLAT_WIN32_FLAG_USE_NVDA) || nvda_bound) + return; + if ((nvda_bound = nvda_controller_init())) + return; + g_plat_win32_flags &= ~PLAT_WIN32_FLAG_USE_NVDA; + g_plat_win32_flags |= PLAT_WIN32_FLAG_USE_POWERSHELL; } #endif @@ -1727,9 +1714,7 @@ static bool is_narrator_running_windows(void) #ifdef HAVE_NVDA else if (g_plat_win32_flags & PLAT_WIN32_FLAG_USE_NVDA) { - long res = nvdaController_testIfRunning_func(); - - if (res != 0) + if (nvda_controller_test_if_running() != 0) { /* The running nvda service wasn't found, so revert back to the powershell method @@ -1797,9 +1782,8 @@ static bool accessibility_speak_windows(int speed, else if (g_plat_win32_flags & PLAT_WIN32_FLAG_USE_NVDA) { wchar_t *wc = utf8_to_utf16_string_alloc(speak_text); - long res = nvdaController_testIfRunning_func(); - if (!wc || res != 0) + if (!wc || nvda_controller_test_if_running() != 0) { RARCH_ERR("Error communicating with NVDA.\n"); /* Fallback on powershell immediately and retry */ @@ -1810,12 +1794,12 @@ static bool accessibility_speak_windows(int speed, return accessibility_speak_windows(speed, speak_text, priority); } - nvdaController_cancelSpeech_func(); + nvda_controller_cancel_speech(); if (g_plat_win32_flags & PLAT_WIN32_FLAG_USE_NVDA_BRAILLE) - nvdaController_brailleMessage_func(wc); + nvda_controller_braille_message(wc); else - nvdaController_speakText_func(wc); + nvda_controller_speak_text(wc); free(wc); } #endif diff --git a/griffin/griffin.c b/griffin/griffin.c index 55d16bd7bf..4804e929d8 100644 --- a/griffin/griffin.c +++ b/griffin/griffin.c @@ -1561,6 +1561,9 @@ FRONTEND #if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__) #include "../frontend/drivers/platform_win32.c" +#ifdef HAVE_NVDA +#include "../frontend/drivers/nvda_controller_win32.c" +#endif #endif #if defined(__WINRT__) || defined(WINAPI_FAMILY) && WINAPI_FAMILY == WINAPI_FAMILY_PHONE_APP diff --git a/nvdaControllerClient64.dll b/nvdaControllerClient64.dll deleted file mode 100644 index 6b4fff7884..0000000000 Binary files a/nvdaControllerClient64.dll and /dev/null differ diff --git a/samples/frontend/win32_nvda/Makefile b/samples/frontend/win32_nvda/Makefile new file mode 100644 index 0000000000..f303f4cb2e --- /dev/null +++ b/samples/frontend/win32_nvda/Makefile @@ -0,0 +1,67 @@ +# frontend/drivers/nvda_controller_win32.c against a stand-in NVDA, an +# RPC server in the test, built and run for x64 and x86 both. +# +# make check +# with the mingw-w64 cross compilers, run under wine (WINE64 and +# WINE32 name a wine for each, with its WINEPREFIX); skipped +# where those are missing +# make check CC=gcc +# from an MSYS2 MINGW64 or MINGW32 shell: that one architecture +# +# make check NVDA_DLL=nvdaControllerClient64.dll +# the same calls first through NVDA's own controller client, +# for the x64 test, to hold the stand-in to it +TARGET64 := win32_nvda_test_x64.exe +TARGET32 := win32_nvda_test_x86.exe + +RARCH_DIR := ../../.. +SRC := win32_nvda_test.c +DEPS := $(SRC) $(RARCH_DIR)/frontend/drivers/nvda_controller_win32.c \ + $(RARCH_DIR)/frontend/drivers/nvda_controller_win32.h + +CFLAGS += -std=gnu89 -Wall -Wextra -Wno-unused-parameter \ + -Wdeclaration-after-statement -g1 -O1 -DHAVE_NVDA \ + -I$(RARCH_DIR)/libretro-common/include +LDLIBS += -lrpcrt4 + +CC64 ?= x86_64-w64-mingw32-gcc +CC32 ?= i686-w64-mingw32-gcc +WINE64 ?= wine +WINE32 ?= wine + +NATIVE := $(findstring mingw,$(shell $(CC) -dumpmachine 2>/dev/null)) + +ifneq ($(NATIVE),) +all: win32_nvda_test.exe +win32_nvda_test.exe: $(DEPS) + $(CC) $(CFLAGS) -o $@ $(SRC) $(LDLIBS) +check: all + ./win32_nvda_test.exe $(NVDA_DLL) +else +HAVE64 := $(shell command -v $(CC64) >/dev/null 2>&1 && command -v $(firstword $(WINE64)) >/dev/null 2>&1 && echo yes) +HAVE32 := $(shell command -v $(CC32) >/dev/null 2>&1 && command -v $(firstword $(WINE32)) >/dev/null 2>&1 && echo yes) + +all: $(if $(HAVE64),$(TARGET64)) $(if $(HAVE32),$(TARGET32)) + +$(TARGET64): $(DEPS) + $(CC64) $(CFLAGS) -o $@ $(SRC) $(LDLIBS) +$(TARGET32): $(DEPS) + $(CC32) $(CFLAGS) -o $@ $(SRC) $(LDLIBS) + +check: all +ifeq ($(HAVE64),yes) + WINEDEBUG=-all $(WINE64) ./$(TARGET64) $(NVDA_DLL) +else + @echo "win32_nvda: skipped x64 - no $(CC64) or $(WINE64)" +endif +ifeq ($(HAVE32),yes) + WINEDEBUG=-all $(WINE32) ./$(TARGET32) +else + @echo "win32_nvda: skipped x86 - no $(CC32) or $(WINE32)" +endif +endif + +clean: + rm -f $(TARGET64) $(TARGET32) win32_nvda_test.exe + +.PHONY: all check clean diff --git a/samples/frontend/win32_nvda/win32_nvda_test.c b/samples/frontend/win32_nvda/win32_nvda_test.c new file mode 100644 index 0000000000..1bd595baaf --- /dev/null +++ b/samples/frontend/win32_nvda/win32_nvda_test.c @@ -0,0 +1,330 @@ +/* frontend/drivers/nvda_controller_win32.c against a stand-in NVDA: + * an RPC server in this process, on the endpoint NVDA listens on, for + * the controller interface as widl compiles nvdaController.idl - an + * NDR implementation of the interface other than the client's, whose + * format strings are MIDL's. Held to, for x86 and x64 alike: + * + * - with no NVDA listening, each call returns an RPC status, nonzero, + * and the process goes on; + * - with it listening, each of the four procedures reaches it, in the + * order made, the strings as sent - non-ASCII, empty and long ones + * among them - and each returns 0; + * - a NULL string is refused without a call; after + * nvda_controller_deinit() nothing is sent. + * + * Given a path, the same calls are first made through that controller + * client DLL - NVDA's own - so the stand-in is shown to be the server + * that client talks to. */ + +#include +#include +#include +#include + +#include "../../../frontend/drivers/nvda_controller_win32.c" + +static unsigned failures; + +#define CHECK(c, what) do { if (!(c)) { \ + printf("FAIL: %s (%s:%d)\n", what, __FILE__, __LINE__); \ + failures++; } else printf(" ok %s\n", what); } while (0) + +/* ---- the stand-in NVDA ---- */ + +static char calls[64]; +static unsigned ncalls; +static wchar_t *last_text; + +static void got(char c, const wchar_t *text) +{ + if (ncalls < sizeof(calls) - 1) + calls[ncalls++] = c; + calls[ncalls] = 0; + free(last_text); + last_text = NULL; + if (text) + { + size_t n = wcslen(text) + 1; + last_text = (wchar_t*)malloc(n * sizeof(wchar_t)); + memcpy(last_text, text, n * sizeof(wchar_t)); + } +} + +static error_status_t __stdcall s_test_if_running(void) +{ got('T', NULL); return 0; } +static error_status_t __stdcall s_speak_text(const wchar_t *text) +{ got('S', text); return 0; } +static error_status_t __stdcall s_cancel_speech(void) +{ got('C', NULL); return 0; } +static error_status_t __stdcall s_braille_message(const wchar_t *message) +{ got('B', message); return 0; } + +/* The procedures as widl generates them for nvdaController.idl */ +#ifdef NVDA_CONTROLLER_X64 +static const unsigned char s_proc_format[] = { + 0x32, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x44, 0x01, 0x0a, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x10, 0x00, + 0x32, 0x48, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x46, 0x02, 0x0a, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0b, 0x01, 0x00, 0x00, 0x04, 0x00, + 0x70, 0x00, 0x08, 0x00, 0x10, 0x00, + 0x32, 0x48, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x08, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x44, 0x01, 0x0a, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x10, 0x00, + 0x32, 0x48, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x46, 0x02, 0x0a, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0b, 0x01, 0x00, 0x00, 0x08, 0x00, + 0x70, 0x00, 0x08, 0x00, 0x10, 0x00, + 0x00 +}; +static const unsigned short s_proc_offset[4] = { 0, 32, 70, 102 }; +#else +static const unsigned char s_proc_format[] = { + 0x32, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x44, 0x01, 0x08, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x10, 0x00, + 0x32, 0x48, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x46, 0x02, 0x08, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x0b, 0x01, 0x00, 0x00, 0x04, 0x00, + 0x70, 0x00, 0x04, 0x00, 0x10, 0x00, + 0x32, 0x48, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x04, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x44, 0x01, 0x08, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, 0x10, 0x00, + 0x32, 0x48, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x08, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x46, 0x02, 0x08, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x0b, 0x01, 0x00, 0x00, 0x08, 0x00, + 0x70, 0x00, 0x04, 0x00, 0x10, 0x00, + 0x00 +}; +static const unsigned short s_proc_offset[4] = { 0, 30, 66, 96 }; +#endif + +static const unsigned char s_type_format[] = { + 0x00, 0x00, + 0x11, 0x08, 0x25, 0x5c, + 0x11, 0x08, 0x25, 0x5c, + 0x00 +}; + +static RPC_DISPATCH_FUNCTION s_dispatch[] = { + NdrServerCall2, NdrServerCall2, NdrServerCall2, NdrServerCall2, 0 +}; +static RPC_DISPATCH_TABLE s_dispatch_table = { 4, s_dispatch, 0 }; + +static const SERVER_ROUTINE s_routines[] = { + (SERVER_ROUTINE)s_test_if_running, + (SERVER_ROUTINE)s_speak_text, + (SERVER_ROUTINE)s_cancel_speech, + (SERVER_ROUTINE)s_braille_message +}; + +static const MIDL_SERVER_INFO s_server_info; + +static const RPC_SERVER_INTERFACE s_interface = { + sizeof(RPC_SERVER_INTERFACE), + { { 0xdff50b99, 0xf7fd, 0x4ca7, + { 0xa8, 0x2c, 0xda, 0xeb, 0x3e, 0x02, 0x52, 0x95 } }, { 1, 0 } }, + { { 0x8a885d04, 0x1ceb, 0x11c9, + { 0x9f, 0xe8, 0x08, 0x00, 0x2b, 0x10, 0x48, 0x60 } }, { 2, 0 } }, + &s_dispatch_table, 0, 0, 0, &s_server_info, 0 +}; + +static const MIDL_STUB_DESC s_stub_desc = { + (void*)&s_interface, nvda_rpc_alloc, nvda_rpc_free, { 0 }, + 0, 0, 0, 0, s_type_format, 1, 0x50002, 0, 0x50200ca, 0, 0, 0, 1, + 0, 0, 0 +}; + +static const MIDL_SERVER_INFO s_server_info = { + &s_stub_desc, s_routines, s_proc_format, s_proc_offset, 0, 0, 0, 0 +}; + +static wchar_t s_endpoint[128]; + +static int stand_in_start(void) +{ + RPC_STATUS st; + /* The client's own binding string, less "ncalrpc:[" and "]" */ + wchar_t b[128]; + size_t n; + if (!nvda_binding_string(b, sizeof(b) / sizeof(b[0]))) + return 0; + n = wcslen(b); + if (n < 11) + return 0; + memcpy(s_endpoint, b + 9, (n - 10) * sizeof(wchar_t)); + s_endpoint[n - 10] = 0; + st = RpcServerUseProtseqEpW((RPC_WSTR)L"ncalrpc", + RPC_C_PROTSEQ_MAX_REQS_DEFAULT, (RPC_WSTR)s_endpoint, NULL); + if (st != RPC_S_OK) + return 0; + if (RpcServerRegisterIf((RPC_IF_HANDLE)&s_interface, NULL, NULL) + != RPC_S_OK) + return 0; + return RpcServerListen(1, 20, TRUE) == RPC_S_OK; +} + +static void stand_in_stop(void) +{ + RpcMgmtStopServerListening(NULL); + RpcMgmtWaitServerListen(); + RpcServerUnregisterIf((RPC_IF_HANDLE)&s_interface, NULL, TRUE); +} + +/* Wine's NdrMapCommAndFaultStatus writes the comm status, then a zero + * fault status - and where both are the return value, as NVDA's are, + * the zero is what is returned. On Windows the comm status is. Under + * Wine a call that cannot reach NVDA is held to returning at all. */ +static int under_wine; + +static int failed_call(unsigned long st) +{ + return st != 0 || under_wine; +} + +/* ---- the calls, through this client or another ---- */ + +struct client +{ + unsigned long (*test_if_running)(void); + unsigned long (*cancel_speech)(void); + unsigned long (*speak_text)(const wchar_t *); + unsigned long (*braille_message)(const wchar_t *); + const char *name; +}; + +static void calls_reach(const struct client *c) +{ + static wchar_t long_text[2049]; + char msg[160]; + int i; + const wchar_t *unicode = L"h\x00e9llo w\x00f6rld \x2713 \xd83d\xde00"; + + for (i = 0; i < 2048; i++) + long_text[i] = (wchar_t)(L'a' + i % 26); + long_text[2048] = 0; + ncalls = 0; + calls[0] = 0; + +#define C(cond, what) do { snprintf(msg, sizeof(msg), "%s: %s", c->name, what); \ + CHECK(cond, msg); } while (0) + C(c->test_if_running() == 0, "testIfRunning reaches NVDA"); + C(c->cancel_speech() == 0, "cancelSpeech reaches NVDA"); + C(c->speak_text(unicode) == 0 && last_text + && !wcscmp(last_text, unicode), "speakText, non-ASCII, as sent"); + C(c->braille_message(L"braille") == 0 && last_text + && !wcscmp(last_text, L"braille"), "brailleMessage as sent"); + C(c->speak_text(L"") == 0 && last_text && !last_text[0], + "an empty string"); + C(c->speak_text(long_text) == 0 && last_text + && !wcscmp(last_text, long_text), "2048 characters"); + C(!strcmp(calls, "TCSBSS"), "each call once, in order"); +#undef C +} + +static unsigned long ours_test(void) +{ return nvda_controller_test_if_running(); } +static unsigned long ours_cancel(void) +{ return nvda_controller_cancel_speech(); } +static unsigned long ours_speak(const wchar_t *t) +{ return nvda_controller_speak_text(t); } +static unsigned long ours_braille(const wchar_t *t) +{ return nvda_controller_braille_message(t); } + +static unsigned long (__stdcall *dll_test)(void); +static unsigned long (__stdcall *dll_cancel)(void); +static unsigned long (__stdcall *dll_speak)(const wchar_t *); +static unsigned long (__stdcall *dll_braille)(const wchar_t *); +static unsigned long via_dll_test(void) { return dll_test(); } +static unsigned long via_dll_cancel(void) { return dll_cancel(); } +static unsigned long via_dll_speak(const wchar_t *t) { return dll_speak(t); } +static unsigned long via_dll_braille(const wchar_t *t) +{ return dll_braille(t); } + +int main(int argc, char **argv) +{ + static const struct client ours = { + ours_test, ours_cancel, ours_speak, ours_braille, "this client" }; + static const struct client theirs = { + via_dll_test, via_dll_cancel, via_dll_speak, via_dll_braille, + "the DLL" }; + + under_wine = GetProcAddress(GetModuleHandleA("ntdll.dll"), + "wine_get_version") != NULL; + printf("%s%s\n", sizeof(void*) == 8 ? "x64" : "x86", + under_wine ? ", under Wine" : ""); + + CHECK(nvda_controller_test_if_running() != 0, + "before init: nothing sent"); + CHECK(nvda_controller_init(), "bound to the NVDA endpoint"); + CHECK(nvda_controller_init(), "bound again: kept"); + + /* No NVDA */ + CHECK(failed_call(nvda_controller_test_if_running()), + "no NVDA: testIfRunning fails"); + CHECK(failed_call(nvda_controller_speak_text(L"x")), + "no NVDA: speakText fails"); + CHECK(failed_call(nvda_controller_braille_message(L"x")), + "no NVDA: brailleMessage fails"); + CHECK(failed_call(nvda_controller_cancel_speech()), + "no NVDA: cancelSpeech fails"); + + CHECK(stand_in_start(), "the stand-in NVDA listening"); + + if (argc > 1) + { + HMODULE dll = LoadLibraryA(argv[1]); + CHECK(dll != NULL, "the controller client DLL loaded"); + if (dll) + { + dll_test = (unsigned long (__stdcall*)(void))(void (*)(void)) + GetProcAddress(dll, "nvdaController_testIfRunning"); + dll_cancel = (unsigned long (__stdcall*)(void))(void (*)(void)) + GetProcAddress(dll, "nvdaController_cancelSpeech"); + dll_speak = (unsigned long (__stdcall*)(const wchar_t*)) + (void (*)(void))GetProcAddress(dll, "nvdaController_speakText"); + dll_braille = (unsigned long (__stdcall*)(const wchar_t*)) + (void (*)(void))GetProcAddress(dll, + "nvdaController_brailleMessage"); + if (dll_test && dll_cancel && dll_speak && dll_braille) + calls_reach(&theirs); + else + CHECK(0, "the DLL's four exports"); + } + } + + calls_reach(&ours); + + ncalls = 0; + CHECK(nvda_controller_speak_text(NULL) == RPC_X_NULL_REF_POINTER + && ncalls == 0, "a NULL string refused, nothing sent"); + + stand_in_stop(); + ncalls = 0; + CHECK(failed_call(nvda_controller_test_if_running()) && ncalls == 0, + "NVDA gone: testIfRunning fails again"); + + nvda_controller_deinit(); + ncalls = 0; + CHECK(nvda_controller_test_if_running() == RPC_S_INVALID_BINDING + && nvda_controller_speak_text(L"x") == RPC_S_INVALID_BINDING + && ncalls == 0, "after deinit: nothing sent"); + + free(last_text); + if (failures) + { + printf("%u failure(s)\n", failures); + return 1; + } + printf("PASS win32_nvda_test\n"); + return 0; +}