win32: NVDA spoken to over RPC, nvdaControllerClient64.dll gone

frontend/drivers/nvda_controller_win32.c talks to NVDA's controller
interface over local RPC itself, as NVDA's controller client does:
bound to ncalrpc:[NvdaCtlr.<session>.<desktop>], its four procedures
called through NdrClientCall2 with MIDL's format strings - the x64 ones
NVDA's client carries, byte for byte, and the same procedures for the
x86 stack - so 32-bit builds reach NVDA too. rpcrt4 and the session and
desktop calls are resolved at run time; where any is missing,
PowerShell speaks as it does without NVDA.

samples/frontend/win32_nvda runs it, x64 and x86, against a stand-in
NVDA in the test: an RPC server for the interface as widl compiles it.
Every procedure arrives in order with its string as sent, non-ASCII,
empty and long ones among them; with no NVDA listening or after deinit
nothing arrives and the calls fail.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-10-29 22:21:11 +01:00
1 parent e9a553a660
commit 9f366c2e01
9 files changed
+815 -37

No files matched your search

-1
View File
@@ -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:
+1
View File
@@ -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
+350
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
/* NVDA's controller interface over local RPC, as NVDA's controller
* client DLL speaks it.
*
* NVDA listens on ncalrpc:[NvdaCtlr.<session id>.<desktop name>] 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 <stdlib.h>
#include <string.h>
#include <windows.h>
#include <rpc.h>
#include <rpcndr.h>
#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.<session>.<desktop>] */
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 */
+44
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef __RARCH_NVDA_CONTROLLER_WIN32_H
#define __RARCH_NVDA_CONTROLLER_WIN32_H
#include <wchar.h>
#include <boolean.h>
#include <retro_common_api.h>
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
+20 -36
View File
@@ -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
+3
View File
@@ -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
Binary file not shown.
+67
View File
@@ -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
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#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;
}