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;
+}