├── .github └── workflows │ └── release.yml ├── .gitignore ├── Makefile ├── README.md ├── bridge-steam.sh ├── install-bridge.sh └── main.c /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: master 6 | 7 | jobs: 8 | Release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: "Checkout" 14 | uses: "actions/checkout@v2.3.4" 15 | 16 | - name: Update package repositories 17 | run: sudo apt-get update 18 | 19 | - name: Install mingw 20 | run: sudo apt-get install -y gcc-mingw-w64 21 | 22 | - name: Build 23 | run: make bridge.exe 24 | 25 | - name: "Release" 26 | uses: "marvinpinto/action-automatic-releases@latest" 27 | with: 28 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 29 | automatic_release_tag: "latest" 30 | prerelease: false 31 | title: "Latest build" 32 | files: | 33 | bridge.exe 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Windows template 3 | # Windows thumbnail cache files 4 | Thumbs.db 5 | Thumbs.db:encryptable 6 | ehthumbs.db 7 | ehthumbs_vista.db 8 | 9 | # Dump file 10 | *.stackdump 11 | 12 | # Folder config file 13 | [Dd]esktop.ini 14 | 15 | # Recycle Bin used on file shares 16 | $RECYCLE.BIN/ 17 | 18 | # Windows Installer files 19 | *.cab 20 | *.msi 21 | *.msix 22 | *.msm 23 | *.msp 24 | 25 | # Windows shortcuts 26 | *.lnk 27 | 28 | ### Linux template 29 | *~ 30 | 31 | # temporary files which can be created if a process still has a handle open of a deleted file 32 | .fuse_hidden* 33 | 34 | # KDE directory preferences 35 | .directory 36 | 37 | # Linux trash folder which might appear on any partition or disk 38 | .Trash-* 39 | 40 | # .nfs files are created when an open file is removed but is still being accessed 41 | .nfs* 42 | 43 | ### macOS template 44 | # General 45 | .DS_Store 46 | .AppleDouble 47 | .LSOverride 48 | 49 | # Icon must end with two \r 50 | Icon 51 | 52 | # Thumbnails 53 | ._* 54 | 55 | # Files that might appear in the root of a volume 56 | .DocumentRevisions-V100 57 | .fseventsd 58 | .Spotlight-V100 59 | .TemporaryItems 60 | .Trashes 61 | .VolumeIcon.icns 62 | .com.apple.timemachine.donotpresent 63 | 64 | # Directories potentially created on remote AFP share 65 | .AppleDB 66 | .AppleDesktop 67 | Network Trash Folder 68 | Temporary Items 69 | .apdisk 70 | 71 | ### JetBrains template 72 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 73 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 74 | 75 | # User-specific stuff 76 | .idea/**/workspace.xml 77 | .idea/**/tasks.xml 78 | .idea/**/usage.statistics.xml 79 | .idea/**/dictionaries 80 | .idea/**/shelf 81 | 82 | # Generated files 83 | .idea/**/contentModel.xml 84 | 85 | # Sensitive or high-churn files 86 | .idea/**/dataSources/ 87 | .idea/**/dataSources.ids 88 | .idea/**/dataSources.local.xml 89 | .idea/**/sqlDataSources.xml 90 | .idea/**/dynamic.xml 91 | .idea/**/uiDesigner.xml 92 | .idea/**/dbnavigator.xml 93 | 94 | # Gradle 95 | .idea/**/gradle.xml 96 | .idea/**/libraries 97 | 98 | # Gradle and Maven with auto-import 99 | # When using Gradle or Maven with auto-import, you should exclude module files, 100 | # since they will be recreated, and may cause churn. Uncomment if using 101 | # auto-import. 102 | # .idea/artifacts 103 | # .idea/compiler.xml 104 | # .idea/jarRepositories.xml 105 | # .idea/modules.xml 106 | # .idea/*.iml 107 | # .idea/modules 108 | # *.iml 109 | # *.ipr 110 | 111 | # CMake 112 | cmake-build-*/ 113 | 114 | # Mongo Explorer plugin 115 | .idea/**/mongoSettings.xml 116 | 117 | # File-based project format 118 | *.iws 119 | 120 | # IntelliJ 121 | out/ 122 | 123 | # mpeltonen/sbt-idea plugin 124 | .idea_modules/ 125 | 126 | # JIRA plugin 127 | atlassian-ide-plugin.xml 128 | 129 | # Cursive Clojure plugin 130 | .idea/replstate.xml 131 | 132 | # Crashlytics plugin (for Android Studio and IntelliJ) 133 | com_crashlytics_export_strings.xml 134 | crashlytics.properties 135 | crashlytics-build.properties 136 | fabric.properties 137 | 138 | # Editor-based Rest Client 139 | .idea/httpRequests 140 | 141 | # Android studio 3.1+ serialized cache file 142 | .idea/caches/build_file_checksums.ser 143 | 144 | .idea 145 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC = x86_64-w64-mingw32-gcc 2 | 3 | CFLAGS = -O2 -Wall 4 | 5 | default: all 6 | all: bridge.exe 7 | 8 | bridge.exe: main.c 9 | $(CC) -masm=intel -mwindows -o $@ $(CFLAGS) $< 10 | 11 | clean: 12 | rm -v bridge.exe 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # macOS Wine Bridge 2 | 3 | This binary allows other binaries running under Wine to communicate to Discord running on the host machine. This binary is designed to work only on 64 bit macOS host systems. 4 | 5 | **If you are using Wine 6.21 or higher (CX22 or higher)**, please see the [wine6.21](https://github.com/Techno-coder/macOS-wine-bridge/tree/wine6.21) branch. 6 | 7 | ## Installation 8 | Note: These instructions assume the usage of a [Wineskin](http://wineskin.urgesoftware.com/tiki-index.php) wrapper. 9 | 1. Download the `bridge.exe` binary from the [releases page](https://github.com/Techno-coder/macOS-wine-bridge/releases) 10 | 2. Move the binary into the Wineskin `drive_c` folder 11 | 3. Invoke the binary before the main application with a batch script: 12 | ```bat 13 | start C:\\bridge.exe 14 | start C:\\.exe 15 | ``` 16 | 4. Change the Wineskin executable path to the batch script: 17 | ``` 18 | "C:\\execute.bat" 19 | ``` 20 | 21 | ## Technical information 22 | 23 | This fork is based on three other projects: 24 | * https://github.com/0e4ef622/wine-discord-ipc-bridge 25 | * https://github.com/koukuno/wine-discord-ipc-bridge 26 | * https://github.com/goeo-/discord-rpc/blob/xnu-under-wine/src/connection_win.cpp 27 | 28 | For general information on how the bridge operates see the mentioned projects. The rest of this section will discuss the specifics for bridging on macOS. 29 | 30 | The `discord-rpc` project created by `goeo-` is unfortunately outdated for two main reasons. The first is that Discord has deprecated the `discord-rpc.dll` method of performing communication in favour of an integrated game sdk file. This makes it difficult to inject code that communicates with the host system rather than with Windows. 31 | 32 | However, much of the code responsible for communicating with Linux and macOS is still valid (see branches `linux-under-wine` and `xnu-under-wine`). In fact, 0e4ef622's implementation directly splices the system call invocations for Linux into the bridging binary. For reference, Wine does not intercept system calls directly but instead replaces Windows API calls. This means that PE binaries can make system calls directly to the host system and it is this method that allows bridging to become possible. 33 | 34 | The system calls used by the `xnu-under-wine` however cannot be used on modern versions of macOS as 32 bit code execution has been disabled. This includes 32 bit system calls which the handlers have not been specifically removed but remain disabled. These handlers can be enabled again by setting the boot argument: `nvram boot-args="no32exec=0"` but this option is impractical for most users as it requires booting into Recovery Mode. This branch uses 32 bit system calls which means that they cause a segmentation fault upon execution. 35 | 36 | Fortunately it does not seem immediately difficult to change these into 64 bit system calls. Add the value `0x2000000` to the call vector register (`eax`) and then change the invocation code from `int 0x80` to `syscall` (https://filippo.io/making-system-calls-from-assembly-in-mac-os-x/). The new system call method also requires the arguments to be placed in registers according to the System V ABI. After these modifications are made, the executable can be recompiled. 37 | 38 | However, after doing so the binary crashes on execution. Upon further inspection (by running the binary through `wine64` directly; note that Gcenx provides a [brew tap](https://github.com/Gcenx/homebrew-wine) that has a formula for running 32 bit applications on Catalina) it is discovered that it crashes on the `syscall` instruction. This is because the compiler used for the original bridges generates a 32 bit executable that causes Wine to request the code be located in a 32 bit segment. This prevents the `syscall` instruction from being executed under restrictions by the Intel processor and hence issues a fault. 39 | 40 | Thankfully the fix is simple. Switch the compiler from `i686-w64-mingw32-gcc` to `x86_64-w64-mingw32-gcc` and the toolchain will generate a 64 bit executable. Note that the registers in the `syscall` instruction have been changed to their long mode form. Finally, note that the calling conventions for the system call wrappers have been changed using an `attribute`. These conventions can be found in GCC's manual and are compatible with mingw's. 41 | 42 | ## Acknowledgements 43 | 44 | - [@jacksonrakena](https://github.com/jacksonrakena) for migrating the bridge to Wine 6.21 45 | -------------------------------------------------------------------------------- /bridge-steam.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Run a Steam Play game with macOS-wine-bridge 4 | # Set the game's launch options to: /path/to/this-script.sh %command% 5 | basedir=$(dirname "$(readlink -f $0)") 6 | 7 | BRIDGE="$basedir/bridge.exe" 8 | DELAY=10 # how many seconds to wait after starting the bridge before starting the game 9 | 10 | "$1" run "$BRIDGE" & 11 | sleep "$DELAY" 12 | "$1" run "${@:3}" 13 | 14 | -------------------------------------------------------------------------------- /install-bridge.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # where are we? 5 | basedir=$(dirname "$(readlink -f $0)") 6 | action=$1 7 | 8 | if [ -z "$action" ]; then 9 | echo "usage: $0 [install/uninstall]" 10 | exit 1 11 | fi 12 | 13 | [ -z "$wine_bin" ] && wine_bin="wine" 14 | 15 | wine_ver=$($wine_bin --version | grep wine) 16 | if [ -z "$wine_ver" ]; then 17 | echo "$wine_bin: "'broken wine installation' >&2 18 | exit 1 19 | fi 20 | 21 | echo "$wine_ver" 22 | 23 | windows_dir=$($wine_bin winepath -u 'C:\windows' 2>/dev/null) 24 | windows_dir="$(echo -n "$windows_dir" | sed 's/\r//g')" 25 | 26 | install() 27 | { 28 | cp -v "$basedir/bridge.exe" "$windows_dir/bridge.exe" 29 | $wine_bin reg add 'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunServices' /v 'bridge' /d 'C:\windows\bridge.exe' /f >/dev/null 2>&1 30 | } 31 | 32 | uninstall() 33 | { 34 | rm -v "$windows_dir/bridge.exe" 35 | $wine_bin reg delete 'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunServices' /v 'bridge.exe' /f >/dev/null 2>&1 36 | } 37 | 38 | $action 39 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | struct sockaddr_un { 8 | unsigned short sun_family; /* AF_UNIX */ 9 | char sun_path[108]; /* pathname */ 10 | }; 11 | 12 | // #define AF_UNIX 0x0001 13 | // #define SOCK_STREAM 0x0001 14 | #define F_SETFL 0x0004 15 | #define O_RDONLY 0x0000 16 | #define O_WRONLY 0x0001 17 | #define O_CREAT 0x0200 18 | #define O_APPEND 0x0008 19 | #define O_NONBLOCK 0x0004 20 | #define BUFSIZE 2048 // size of read/write buffers 21 | 22 | __declspec(naked) void __syscall() { 23 | __asm__ ( 24 | "__syscall:\n\t" 25 | "add rax, 0x2000000\n\t" 26 | 27 | "syscall\n\t" 28 | "jnc noerror\n\t" 29 | "neg rax\n\t" 30 | 31 | "noerror:\n\t" 32 | "ret" 33 | ); 34 | } 35 | 36 | // technocoder: sysv_abi must be used for x86_64 unix system calls 37 | __declspec(naked) __attribute__((sysv_abi)) unsigned int l_getpid() { 38 | __asm__ ( 39 | "mov eax, 0x14\n\t" 40 | "jmp __syscall\n\t" 41 | "ret" 42 | ); 43 | } 44 | __declspec(naked) __attribute__((sysv_abi)) int l_close(int fd) { 45 | __asm__ ( 46 | "mov eax, 0x06\n\t" 47 | "jmp __syscall\n\t" 48 | "ret" 49 | ); 50 | } 51 | __declspec(naked) __attribute__((sysv_abi)) int l_fcntl(unsigned int fd, unsigned int cmd, unsigned long arg) { 52 | __asm__ ( 53 | "mov eax, 0x5c\n\t" 54 | "jmp __syscall\n\t" 55 | "ret" 56 | ); 57 | } 58 | __declspec(naked) __attribute__((sysv_abi)) int l_open(const char* filename, int flags, int mode) { 59 | __asm__ ( 60 | "mov eax, 0x05\n\t" 61 | "jmp __syscall\n\t" 62 | "ret" 63 | ); 64 | } 65 | __declspec(naked) __attribute__((sysv_abi)) int l_write(unsigned int fd, const char* buf, unsigned int count) { 66 | __asm__ ( 67 | "mov eax, 0x04\n\t" 68 | "jmp __syscall\n\t" 69 | "ret" 70 | ); 71 | } 72 | __declspec(naked) __attribute__((sysv_abi)) int l_read(unsigned int fd, char* buf, unsigned int count) { 73 | __asm__ ( 74 | "mov eax, 0x03\n\t" 75 | "jmp __syscall\n\t" 76 | "ret" 77 | ); 78 | } 79 | __declspec(naked) __attribute__((sysv_abi)) int l_socket(int domain, int type, int protocol) { 80 | __asm__ ( 81 | "mov eax, 0x61\n\t" 82 | "jmp __syscall\n\t" 83 | "ret" 84 | ); 85 | } 86 | __declspec(naked) __attribute__((sysv_abi)) int l_connect(int sockfd, const struct sockaddr *addr, unsigned int addrlen) { 87 | __asm__ ( 88 | "mov eax, 0x62\n\t" 89 | "jmp __syscall\n\t" 90 | "ret" 91 | ); 92 | } 93 | 94 | static const char* get_temp_path() 95 | { 96 | const char* temp = getenv("TMPDIR"); 97 | temp = temp ? temp : "/tmp"; 98 | return temp; 99 | } 100 | 101 | static HANDLE hPipe = INVALID_HANDLE_VALUE; 102 | static int sock_fd; 103 | DWORD WINAPI winwrite_thread(LPVOID lpvParam); 104 | 105 | // koukuno: This implemented because if no client ever connects but there are no more user 106 | // wine processes running, this bridge can just exit gracefully. 107 | static HANDLE conn_evt = INVALID_HANDLE_VALUE; 108 | static BOOL fConnected = FALSE; 109 | 110 | static HANDLE make_wine_system_process() 111 | { 112 | HMODULE ntdll_mod; 113 | FARPROC proc; 114 | 115 | if ((ntdll_mod = GetModuleHandleW(L"NTDLL.DLL")) == NULL) { 116 | printf("Cannot find NTDLL.DLL in process map"); 117 | return NULL; 118 | } 119 | 120 | if ((proc = GetProcAddress(ntdll_mod, "__wine_make_process_system")) == NULL) { 121 | printf("Not a wine installation?"); 122 | return NULL; 123 | } 124 | 125 | return ((HANDLE (CDECL *)(void))proc)(); 126 | } 127 | 128 | DWORD WINAPI wait_for_client(LPVOID param) 129 | { 130 | (void)param; 131 | 132 | fConnected = ConnectNamedPipe(hPipe, NULL) ? 133 | TRUE : (GetLastError() == ERROR_PIPE_CONNECTED); 134 | 135 | SetEvent(conn_evt); 136 | return 0; 137 | } 138 | 139 | int main(void) 140 | { 141 | DWORD dwThreadId = 0; 142 | HANDLE hThread = NULL; 143 | HANDLE wine_evt = NULL; 144 | 145 | if ((wine_evt = make_wine_system_process()) == NULL) { 146 | return 1; 147 | } 148 | 149 | // The main loop creates an instance of the named pipe and 150 | // then waits for a client to connect to it. When the client 151 | // connects, a thread is created to handle communications 152 | // with that client, and this loop is free to wait for the 153 | // next client connect request. It is an infinite loop. 154 | 155 | printf("Opening discord-ipc-0 Windows pipe\n"); 156 | hPipe = CreateNamedPipeW( 157 | L"\\\\.\\pipe\\discord-ipc-0", // pipe name 158 | PIPE_ACCESS_DUPLEX, // read/write access 159 | PIPE_TYPE_BYTE | // message type pipe 160 | PIPE_READMODE_BYTE | // message-read mode 161 | PIPE_WAIT, // blocking mode 162 | 1, // max. instances 163 | BUFSIZE, // output buffer size 164 | BUFSIZE, // input buffer size 165 | 0, // client time-out 166 | NULL); // default security attribute 167 | 168 | if (hPipe == INVALID_HANDLE_VALUE) 169 | { 170 | printf("CreateNamedPipe failed, GLE=%lu.\n", GetLastError()); 171 | return -1; 172 | } 173 | 174 | conn_evt = CreateEventW(NULL, FALSE, FALSE, NULL); 175 | CloseHandle(CreateThread(NULL, 0, wait_for_client, NULL, 0, NULL)); 176 | for (;;) { 177 | HANDLE events[] = { wine_evt, conn_evt }; 178 | DWORD result = WaitForMultipleObjectsEx(2, events, FALSE, 0, FALSE); 179 | if (result == WAIT_TIMEOUT) 180 | continue; 181 | 182 | if (result == 0) { 183 | printf("Bridge exiting, wine closing\n"); 184 | } 185 | 186 | break; 187 | } 188 | 189 | if (fConnected) 190 | { 191 | printf("Client connected\n"); 192 | 193 | printf("Creating socket\n"); 194 | 195 | if ((sock_fd = l_socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { 196 | printf("Failed to create socket\n"); 197 | return 1; 198 | } 199 | 200 | printf("Socket created\n"); 201 | 202 | struct sockaddr_un addr; 203 | addr.sun_family = AF_UNIX; 204 | 205 | const char *const temp_path = get_temp_path(); 206 | 207 | char connected = 0; 208 | for (int pipeNum = 0; pipeNum < 10; ++pipeNum) { 209 | 210 | snprintf(addr.sun_path, sizeof(addr.sun_path), "%sdiscord-ipc-%d", temp_path, pipeNum); 211 | printf("Attempting to connect to %s\n", addr.sun_path); 212 | 213 | if (l_connect(sock_fd, (struct sockaddr*) &addr, sizeof(addr)) < 0) { 214 | printf("Failed to connect\n"); 215 | } else { 216 | connected = 1; 217 | break; 218 | } 219 | } 220 | 221 | if (!connected) { 222 | printf("Could not connect to discord client\n"); 223 | return 1; 224 | } 225 | 226 | 227 | printf("Connected successfully\n"); 228 | 229 | hThread = CreateThread( 230 | NULL, // no security attribute 231 | 0, // default stack size 232 | winwrite_thread, // thread proc 233 | (LPVOID) NULL, // thread parameter 234 | 0, // not suspended 235 | &dwThreadId); // returns thread ID 236 | 237 | if (hThread == NULL) 238 | { 239 | printf("CreateThread failed, GLE=%lu.\n", GetLastError()); 240 | return 1; 241 | } 242 | 243 | 244 | for (;;) { 245 | char buf[BUFSIZE]; 246 | DWORD bytes_read = 0; 247 | BOOL fSuccess = ReadFile( 248 | hPipe, // handle to pipe 249 | buf, // buffer to receive data 250 | BUFSIZE, // size of buffer 251 | &bytes_read, // number of bytes read 252 | NULL); // not overlapped I/O 253 | if (!fSuccess) { 254 | if (GetLastError() == ERROR_BROKEN_PIPE) { 255 | printf("winread EOF\n"); 256 | return 0; 257 | } else { 258 | printf("Failed to read from pipe\n"); 259 | return 1; 260 | } 261 | } 262 | 263 | printf("%ld bytes w->l\n", bytes_read); 264 | /* uncomment to dump the actual data being passed from the pipe to the socket */ 265 | /* for(int i=0;iw\n", bytes_read); 306 | /* uncomment to dump the actual data being passed from the socket to the pipe */ 307 | /* for(int i=0;i