10 Commits

Author SHA1 Message Date
299408d10e Update Makefile to use x86_64-w64-mingw32-gcc 2024-03-07 06:48:54 +02:00
8b356b65b8 Add macOS support 2024-03-07 06:48:50 +02:00
66a20f51c4 Update platform check and add help option 2024-03-07 06:48:29 +02:00
d5af2277c5 Prevent installing as service on macOS 2024-03-07 06:47:56 +02:00
16291ec0fd Add macOS instructions 2024-03-07 06:47:35 +02:00
56168a6d3d Add socket system call constants 2024-01-26 03:35:34 +02:00
d2b23eb2e8 Check if it's running under Linux 2024-01-26 03:34:00 +02:00
cba4fa1e2b Update README.md 2023-12-17 01:25:19 +02:00
8c7b336c0e Update README.md 2023-10-13 19:20:39 +03:00
334c4e0479 Update README.md 2023-10-13 04:21:42 +03:00
5 changed files with 247 additions and 59 deletions

View File

@ -14,11 +14,11 @@ install: build
build: $(C_OBJECTS)
$(info Linking)
i686-w64-mingw32-gcc $(C_OBJECTS) $(LFLAGS) $(DBGFLAGS) -o build/bridge.exe
x86_64-w64-mingw32-gcc $(C_OBJECTS) $(LFLAGS) $(DBGFLAGS) -o build/bridge.exe
%.o: %.c
$(info Compiling $<)
i686-w64-mingw32-gcc $(CFLAGS) $(DBGFLAGS) -c $< -o $@
x86_64-w64-mingw32-gcc $(CFLAGS) $(DBGFLAGS) -c $< -o $@
clean:
rm -f $(C_OBJECTS) build/bridge.exe

View File

@ -1,5 +1,9 @@
# Discord RPC Bridge for Wine
![GitHub](https://img.shields.io/github/license/EnderIce2/rpc-bridge)
![GitHub All Releases](https://img.shields.io/github/downloads/EnderIce2/rpc-bridge/total)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/EnderIce2/rpc-bridge)
Simple bridge that allows you to use Discord Rich Presence with Wine games/software.
Works by running a small program in the background that creates a [named pipe](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipes) `\\.\pipe\discord-ipc-0` inside the prefix and forwards all data to the pipe `/run/user/1000/discord-ipc-0`.
@ -13,12 +17,13 @@ This bridge takes advantage of the Windows service implementation in Wine, elimi
- [Discord RPC Bridge for Wine](#discord-rpc-bridge-for-wine)
- [Table of Contents](#table-of-contents)
- [Installation \& Usage](#installation--usage)
- [Installing inside a prefix](#installing-inside-a-prefix)
- [Wine (~/.wine)](#wine-wine)
- [Lutris](#lutris)
- [Steam](#steam)
- [If you use Flatpak](#if-you-use-flatpak)
- [Installing inside a prefix](#installing-inside-a-prefix)
- [Wine (~/.wine)](#wine-wine)
- [Lutris](#lutris)
- [Steam](#steam)
- [If you use Flatpak](#if-you-use-flatpak)
- [Run without installing the service](#run-without-installing-the-service)
- [About macOS](#about-macos)
- [Compiling from source](#compiling-from-source)
- [Command line arguments](#command-line-arguments)
- [Debugging](#debugging)
@ -76,7 +81,7 @@ If you prefer not to use the service for any reason, please refer to the [Run wi
## Run without installing the service
If you prefer not to use the service, you can manually run `bridge.exe`` within the Wine prefix.
If you prefer not to use the service, you can manually run `bridge.exe` within the Wine prefix.
This method is compatible with both Wine and Lutris.
In Lutris, you can achieve this by adding the path to `bridge.exe` in the `Executable` field under `Game options`. In `Arguments` field, be sure to include the _Windows_ path to the game's executable.
@ -92,9 +97,15 @@ In Wine, all you need to do is run `bridge.exe`.
- When running the program manually without providing any arguments, it will simply initiate the bridge and wait indefinitely until it's closed.
## About macOS
The bridge works similarly on macOS as it does on Linux, but it can't be registered as a service due to TMPDIR limitations. macOS users must manually run `bridge.exe` when needed.
To run `bridge.exe` on macOS, navigate to its directory in the terminal and execute `wine bridge.exe`. (or double click it in the Finder)
## Compiling from source
- Install the `wine`, `i686-w64-mingw32-gcc` and `make` packages.
- Install the `wine`, `x86_64-w64-mingw32-gcc` and `make` packages.
- Open a terminal in the directory that contains this file and run `make`.
- The compiled executable will be located in `build/bridge.exe`.

229
bridge.c
View File

@ -4,22 +4,48 @@
#include <assert.h>
#include <stdio.h>
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_socketcall 102
#define __linux_read 3
#define __linux_write 4
#define __linux_open 5
#define __linux_close 6
#define __linux_munmap 91
#define __linux_socketcall 102
#define __linux_mmap2 192
#define __linux_socket 359
#define __linux_connect 362
#define __darwin_read 0x2000003
#define __darwin_write 0x2000004
#define __darwin_open 0x2000005
#define __darwin_close 0x2000006
#define __darwin_socket 0x2000061
#define __darwin_connect 0x2000062
#define __darwin_fcntl 0x200005C
#define __darwin_sysctl 0x20000CA
#define O_RDONLY 00
#define PROT_READ 1
#define PROT_WRITE 2
#define MAP_PRIVATE 0x02
#define MAP_FIXED 0x10
#define MAP_ANON 0x20
#define MAP_FAILED ((void *)-1)
#define SYS_SOCKET 1
#define SYS_CONNECT 3
#define likely(expr) (__builtin_expect(!!(expr), 1))
#define unlikely(expr) (__builtin_expect(!!(expr), 0))
#define force_inline \
__inline__ \
__attribute__((__always_inline__, __gnu_inline__))
#define naked __attribute__((naked))
typedef unsigned short sa_family_t;
typedef char *caddr_t;
typedef unsigned socklen_t;
struct sockaddr_un
{
sa_family_t sun_family; /* AF_UNIX */
@ -36,10 +62,11 @@ void print(char const *fmt, ...);
LPTSTR GetErrorMessage();
extern BOOL RunningAsService;
BOOL RetryNewConnection;
BOOL IsLinux;
static force_inline int syscall(int num,
intptr_t arg1, intptr_t arg2, intptr_t arg3,
intptr_t arg4, intptr_t arg5, intptr_t arg6)
static force_inline int linux_syscall(int num,
int arg1, int arg2, int arg3,
int arg4, int arg5, int arg6)
{
int ret;
__asm__ __volatile__(
@ -48,38 +75,124 @@ static force_inline int syscall(int num,
: "0"(num), "b"(arg1), "c"(arg2),
"d"(arg3), "S"(arg4), "D"(arg5)
: "memory");
return ret;
}
static naked int darwin_syscall(int num,
long arg1, long arg2, long arg3,
long arg4, long arg5, long arg6)
{
register long r10 __asm__("r10") = arg4;
register long r8 __asm__("r8") = arg5;
register long r9 __asm__("r9") = arg6;
__asm__ __volatile__(
"syscall\n"
"jae noerror\n"
"negq %%rax\n"
"noerror:\n"
"ret\n"
: "=a"(num)
: "a"(num), "D"(arg1), "S"(arg2), "d"(arg3), "r"(r10), "r"(r8), "r"(r9)
: "memory");
}
static inline int sys_read(int fd, void *buf, size_t count)
{
return syscall(__NR_read, fd, (intptr_t)buf, count, 0, 0, 0);
if (IsLinux)
return linux_syscall(__linux_read, fd, buf, count, 0, 0, 0);
else
return darwin_syscall(__darwin_read, fd, buf, count, 0, 0, 0);
}
static inline int sys_write(int fd, const void *buf, size_t count)
{
return syscall(__NR_write, fd, (intptr_t)buf, count, 0, 0, 0);
if (IsLinux)
return linux_syscall(__linux_write, fd, buf, count, 0, 0, 0);
else
return darwin_syscall(__darwin_write, fd, buf, count, 0, 0, 0);
}
static inline int sys_open(const char *pathname, int flags, int mode)
{
return syscall(__NR_open, (intptr_t)pathname, flags, mode, 0, 0, 0);
if (IsLinux)
return linux_syscall(__linux_open, pathname, flags, mode, 0, 0, 0);
else
return darwin_syscall(__darwin_open, pathname, flags, mode, 0, 0, 0);
}
static inline int sys_close(int fd)
{
return syscall(__NR_close, fd, 0, 0, 0, 0, 0);
if (IsLinux)
return linux_syscall(__linux_close, fd, 0, 0, 0, 0, 0);
else
return darwin_syscall(__darwin_close, fd, 0, 0, 0, 0, 0);
}
static inline unsigned int *sys_mmap(unsigned int *addr, size_t length, int prot, int flags, int fd, off_t offset)
{
assert(IsLinux);
return linux_syscall(__linux_mmap2, addr, length, prot, flags, fd, offset);
}
static inline int sys_munmap(unsigned int *addr, size_t length)
{
assert(IsLinux);
return linux_syscall(__linux_munmap, addr, length, 0, 0, 0, 0);
}
static inline int sys_socketcall(int call, unsigned long *args)
{
return syscall(__NR_socketcall, call, (intptr_t)args, 0, 0, 0, 0);
assert(IsLinux);
return linux_syscall(__linux_socketcall, call, args, 0, 0, 0, 0);
}
char *linux_getenv(const char *name)
static inline int sys_socket(int domain, int type, int protocol)
{
int fd = sys_open("/proc/self/environ", O_RDONLY, 0);
if (IsLinux)
return linux_syscall(__linux_socket, domain, type, protocol, 0, 0, 0);
else
return darwin_syscall(__darwin_socket, domain, type, protocol, 0, 0, 0);
}
static inline int sys_connect(int s, caddr_t name, socklen_t namelen)
{
if (IsLinux)
return linux_syscall(__linux_connect, s, name, namelen, 0, 0, 0);
else
return darwin_syscall(__darwin_connect, s, name, namelen, 0, 0, 0);
}
char *native_getenv(const char *name)
{
if (!IsLinux)
{
char *value = getenv(name);
if (value == NULL)
{
print("Failed to get environment variable: %s\n", name);
return NULL;
}
return value;
}
/* I hope the 0x10000 is okay */
void *environStr = sys_mmap(0x10000, 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0);
if (environStr == MAP_FAILED)
{
print("Failed to allocate environ string: %d\n", (intptr_t)environStr);
return NULL;
}
if ((uintptr_t)environStr > 0x7effffff)
print("Warning: environStr %p is above 2GB\n", environStr);
const char *linux_environ = "/proc/self/environ";
memcpy(environStr, linux_environ, strlen(linux_environ) + 1);
int fd = sys_open(environStr, O_RDONLY, 0);
sys_munmap(environStr, 4096);
if (fd < 0)
{
print("Failed to open /proc/self/environ: %d\n", fd);
@ -120,22 +233,24 @@ char *linux_getenv(const char *name)
void ConnectToSocket(int fd)
{
print("Connecting to socket\n");
const char *runtime = linux_getenv("XDG_RUNTIME_DIR");
const char *runtime;
if (IsLinux)
runtime = native_getenv("XDG_RUNTIME_DIR");
else
runtime = native_getenv("TMPDIR");
if (runtime == NULL)
{
print("XDG_RUNTIME_DIR not set\n");
print("IPC directory not set\n");
if (!RunningAsService)
{
MessageBox(NULL,
"XDG_RUNTIME_DIR not set",
MessageBox(NULL, "IPC directory not set",
"Environment variable not set",
MB_OK | MB_ICONSTOP);
}
ExitProcess(1);
}
print("XDG_RUNTIME_DIR: %s\n", runtime);
print("IPC directory: %s\n", runtime);
/* TODO: check for multiple discord instances and create a pipe for each */
const char *discordUnixPipes[] = {
"/discord-ipc-0",
"/snap.discord/discord-ipc-0",
@ -156,12 +271,17 @@ void ConnectToSocket(int fd)
print("Connecting to %s\n", pipePath);
unsigned long socketArgs[] = {
(unsigned long)fd,
(unsigned long)&socketAddr,
sizeof(socketAddr)};
if (IsLinux)
{
unsigned long socketArgs[] = {
(unsigned long)fd,
(unsigned long)(intptr_t)&socketAddr,
sizeof(socketAddr)};
sockRet = sys_socketcall(3, socketArgs);
sockRet = sys_socketcall(SYS_CONNECT, socketArgs);
}
else
sockRet = sys_connect(fd, (caddr_t)&socketAddr, sizeof(socketAddr));
free(pipePath);
if (sockRet >= 0)
@ -172,12 +292,9 @@ void ConnectToSocket(int fd)
{
print("socketcall failed for: %d\n", sockRet);
if (!RunningAsService)
{
MessageBox(NULL,
"Failed to connect to Discord",
MessageBox(NULL, "Failed to connect to Discord",
"Socket Connection failed",
MB_OK | MB_ICONSTOP);
}
ExitProcess(1);
}
}
@ -323,6 +440,17 @@ void PipeBufferOutThread(LPVOID lpParam)
void CreateBridge()
{
static void(CDECL * wine_get_host_version)(const char **sysname, const char **release);
HMODULE hntdll = GetModuleHandle("ntdll.dll");
wine_get_host_version = (void *)GetProcAddress(hntdll, "wine_get_host_version");
assert(wine_get_host_version);
const char *__sysname;
const char *__release;
wine_get_host_version(&__sysname, &__release);
IsLinux = strcmp(__sysname, "Linux") == 0;
print("Running on %s\n", __sysname);
LPCTSTR lpszPipename = TEXT("\\\\.\\pipe\\discord-ipc-0");
NewConnection:
@ -330,11 +458,11 @@ NewConnection:
NULL, NULL,
NULL, NULL))
{
print("Pipe already exists: %s\n",
GetErrorMessage());
if (!RunningAsService)
MessageBox(NULL, GetErrorMessage(),
NULL, MB_OK | MB_ICONSTOP);
print("Pipe already exists: %s\n",
GetErrorMessage());
ExitProcess(1);
}
@ -346,11 +474,11 @@ NewConnection:
if (hPipe == INVALID_HANDLE_VALUE)
{
print("Failed to create pipe: %s\n",
GetErrorMessage());
if (!RunningAsService)
MessageBox(NULL, GetErrorMessage(),
NULL, MB_OK | MB_ICONSTOP);
print("Failed to create pipe: %s\n",
GetErrorMessage());
ExitProcess(1);
}
@ -358,30 +486,35 @@ NewConnection:
print("Waiting for pipe connection\n");
if (!ConnectNamedPipe(hPipe, NULL))
{
print("Failed to connect to pipe: %s\n",
GetErrorMessage());
if (!RunningAsService)
MessageBox(NULL, GetErrorMessage(),
NULL, MB_OK | MB_ICONSTOP);
print("Failed to connect to pipe: %s\n",
GetErrorMessage());
ExitProcess(1);
}
print("Pipe connected\n");
unsigned long socketArgs[] = {
(unsigned long)AF_UNIX,
(unsigned long)SOCK_STREAM,
0};
int fd = sys_socketcall(1, socketArgs);
int fd;
if (IsLinux)
{
unsigned long socketArgs[] = {
(unsigned long)AF_UNIX,
(unsigned long)SOCK_STREAM,
0};
fd = sys_socketcall(SYS_SOCKET, socketArgs);
}
else
fd = sys_socket(AF_UNIX, SOCK_STREAM, 0);
print("Socket %d created\n", fd);
if (fd < 0)
{
if (!RunningAsService)
MessageBox(NULL, GetErrorMessage(),
NULL, MB_OK | MB_ICONSTOP);
print("Failed to create socket: %d\n", fd);
if (!RunningAsService)
MessageBox(NULL, "Failed to create socket",
NULL, MB_OK | MB_ICONSTOP);
ExitProcess(1);
}
@ -402,9 +535,9 @@ NewConnection:
if (hIn == NULL || hOut == NULL)
{
print("Failed to create threads: %s\n", GetErrorMessage());
if (!RunningAsService)
MessageBox(NULL, GetErrorMessage(), NULL, MB_OK | MB_ICONSTOP);
print("Failed to create threads: %s\n", GetErrorMessage());
ExitProcess(1);
}

29
main.c
View File

@ -52,6 +52,23 @@ void DetectWine()
GetErrorMessage(), MB_OK | MB_ICONINFORMATION);
ExitProcess(1);
}
static void(CDECL * wine_get_host_version)(const char **sysname, const char **release);
wine_get_host_version = (void *)GetProcAddress(hNTdll, "wine_get_host_version");
assert(wine_get_host_version);
const char *__sysname;
const char *__release;
wine_get_host_version(&__sysname, &__release);
if (strcmp(__sysname, "Linux") != 0 && strcmp(__sysname, "Darwin") != 0)
{
int result = MessageBox(NULL, "This program is designed for Linux and macOS only!\nDo you want to proceed?",
NULL, MB_YESNO | MB_ICONQUESTION);
if (result == IDYES)
return;
else if (result == IDNO)
ExitProcess(1);
}
}
void print(char const *fmt, ...)
@ -97,11 +114,19 @@ int main(int argc, char *argv[])
}
return 0;
}
if (strcmp(argv[1], "--install") == 0)
else if (strcmp(argv[1], "--install") == 0)
InstallService();
else if (strcmp(argv[1], "--uninstall") == 0)
RemoveService();
else if (strcmp(argv[1], "--help") == 0)
{
printf("Usage:\n");
printf(" %s [args]\n", argv[0]);
printf(" --install - Install service\n");
printf(" --uninstall - Uninstall service\n");
printf(" --help - Show this help\n");
ExitProcess(0);
}
}
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)CreateBridge,

View File

@ -83,10 +83,29 @@ void ServiceMain(DWORD argc, LPTSTR *argv)
return;
}
void DetectDarwin()
{
static void(CDECL * wine_get_host_version)(const char **sysname, const char **release);
wine_get_host_version = (void *)GetProcAddress(GetModuleHandle("ntdll.dll"),
"wine_get_host_version");
const char *__sysname;
const char *__release;
wine_get_host_version(&__sysname, &__release);
if (strcmp(__sysname, "Darwin") == 0)
{
/* FIXME: I don't know how to get the TMPDIR without getenv */
MessageBox(NULL, "Registering as a service is not supported on macOS at the moment.",
"Unsupported", MB_OK | MB_ICONINFORMATION);
ExitProcess(1);
}
}
void InstallService()
{
print("Registering to run on startup\n");
DetectDarwin();
SC_HANDLE schSCManager, schService;
DWORD dwTagId;