Introduced Wine64 wrapper

This commit is contained in:
radiomanV
2025-09-20 19:40:10 +03:00
parent 988b9d1373
commit ffd1a826f4
15 changed files with 3394 additions and 4 deletions

View File

@@ -69,6 +69,11 @@ This utility can dump the `infoic.dll` and `infoic2plus.dll` to an XML format da
More information on this here: [InfoicDump](https://github.com/radiomanV/TL866/tree/master/InfoIcDump#infoicdump)
### WineLib wrapper
This library provide an easy way to make Minipro/Xgpro software work in Linux.
More information on this here: [Wine](https://github.com/radiomanV/TL866/tree/master/wine32#)
### 32-bit WineLib wrapper (Unmaintained)
This library provides an easy way to run Minipro/Xgpro software on Linux.
More information: [WineLib wrapper](https://github.com/radiomanV/TL866/tree/master/wine32#)
**Note:** This 32-bit wrapper is no longer maintained.
For the actively maintained wrapper that supports all architectures, see [new wine wrapper](https://github.com/radiomanV/TL866/tree/master/wine64#)

View File

@@ -2,7 +2,7 @@
>
> This wrapper is no longer maintained. It targets 32-bit Wine environments and is provided *as-is* for users
> on systems/Wine versions that still support 32-bit.
> For the actively maintained replacement, see the new wrapper [here](https://github.com/radiomanV/TL866/raw/refs/heads/master/wine64)
> For the actively maintained replacement, see the new wrapper [here](https://github.com/radiomanV/TL866/raw/refs/heads/master/wine64#)
# Simple low level winelib usb wrapper for the Minipro TL866A/CS, TL866II+, Xgecu T48, T56 and T76 programmers.

77
wine64/Makefile Normal file
View File

@@ -0,0 +1,77 @@
CC ?= gcc
CFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter
LDFLAGS ?= -lpthread
WINCC ?= i686-w64-mingw32-gcc
WINRC ?= i686-w64-mingw32-windres
PKG_CONFIG := $(shell which pkg-config 2>/dev/null)
# LibUSB detection
ifeq ($(strip $(PKG_CONFIG)),)
LIBUSB_CFLAGS :=
LIBUSB_LIBS := -lusb-1.0
else
LIBUSB_CFLAGS := $(shell $(PKG_CONFIG) --cflags libusb-1.0 2>/dev/null)
LIBUSB_LIBS := $(shell $(PKG_CONFIG) --libs libusb-1.0 2>/dev/null)
ifeq ($(strip $(LIBUSB_LIBS)),)
LIBUSB_LIBS := -lusb-1.0
endif
endif
# usb-broker
BIN := usb-broker
SRC := broker.c
HDR := protocol.h
RES_RC := resource.rc
RES_RES := resource.res
# shim.dll
DLL := shim.dll
DLL_SRCS := shim.c
DLL_OBJS := $(DLL_SRCS:.c=.o)
DLL_CFLAGS := -O2 -s -DWIN32 -D_WIN32_WINNT=0x0601 -Wall -Wextra -Wno-unused-parameter
DLL_LDFLAGS := -shared -Wl,--kill-at -Wl,--enable-stdcall-fixup
DLL_LIBS := -lws2_32 -lshlwapi -lgdi32 -luxtheme
# launcher.exe
LAUNCHER := launcher.exe
LAUNCHER_SRCS := launcher.c
LAUNCHER_OBJS := $(LAUNCHER_SRCS:.c=.o)
LAUNCHER_CFLAGS := -O2 -s -DWIN32 -D_WIN32_WINNT=0x0601 -Wall -Wextra
LAUNCHER_LDFLAGS:= -municode -mconsole
LAUNCHER_LIBS := -lshlwapi
.PHONY: all broker shim launcher windows clean
all: broker shim launcher
broker: $(BIN)
$(BIN): $(SRC) $(HDR)
$(CC) $(CFLAGS) $(LIBUSB_CFLAGS) -o $@ $(SRC) $(LDFLAGS) $(LIBUSB_LIBS)
windows: shim launcher
shim: $(DLL)
launcher: $(LAUNCHER)
$(DLL): $(DLL_OBJS) $(RES_RES)
$(WINCC) $(DLL_LDFLAGS) -o $@ $(DLL_OBJS) $(RES_RES) $(DLL_LIBS)
$(LAUNCHER): $(LAUNCHER_OBJS)
$(WINCC) $(LAUNCHER_LDFLAGS) -o $@ $(LAUNCHER_OBJS) $(LAUNCHER_LIBS)
%.o: %.c
$(WINCC) $(WCCFLAGS) -c $< -o $@
shim.o: WCCFLAGS := $(DLL_CFLAGS)
launcher.o: WCCFLAGS := $(LAUNCHER_CFLAGS)
$(RES_RES): $(RES_RC) resource.h
$(WINRC) -i $(RES_RC) -O coff -o $(RES_RES)
clean:
rm -f $(BIN) *.o $(DLL) $(LAUNCHER) $(RES_RES)

220
wine64/README.md Normal file
View File

@@ -0,0 +1,220 @@
# Xgpro/Minipro Cross-Architecture USB Wrapper (Broker + Shim)
**What is this?**
A modern, cross-architecture replacement for the legacy 32-bit [WineLib wrapper](https://github.com/radiomanV/TL866/tree/master/wine32#).
It splits the old `setupapi.dll` logic into:
- **A native USB broker** (Linux, 64-bit or 32-bit, any arch) that talks to LibUSB.
- **A native Windows shim DLL** that runs inside the Windows app (Xgpro/Minipro) and proxies USB calls to the broker.
**Why?**
- Works on **x86_64, aarch64, armhf**, etc.
- No 32-bit Wine and 32-bit libs requirement.
- Avoids SSE/stack alignment pitfalls from the legacy approach.
- Cleaner debugging and isolation.
---
## Supported programmers
- **Minipro TL866A/CS**
- **XGecu TL866II+**
- **XGecu T48 / T56 / T76**
## Features
- **Cross-arch**: broker builds natively for your host (x86_64, arm64, armhf, etc.)
- **Broker/Shim split**: minimal 32-Bit native Windows DLL
- **Socket protocol**: shim to broker IPC over local TCP socket
- **Hotplug**: libusb hotplug events
- **Verbose tracing**: single env var to dump full TX/RX
- **Drop-in**: works with stock `Xgpro.exe` / `Xgpro_T76.exe` / `Minipro.exe`
---
## Requirements
### Host (Linux)
- `libusb-1.0` (runtime; `-dev` for building broker)
- `wine` or `wine64` (only runtime, no `-dev` packages needed)
- Build tools: `gcc`, `i686-w64-mingw32-gcc`, `make`, `pkg-config`
```bash
# Debian / Ubuntu / Mint
sudo apt install -y wine winetricks libusb-1.0-0
sudo apt install -y build-essential pkg-config libusb-1.0-0-dev \
mingw-w64 gcc-mingw-w64-i686
# Arch / Manjaro
sudo pacman -S --needed wine winetricks libusb
sudo pacman -S --needed gcc make pkgconf mingw-w64-gcc
# Fedora / RHEL
sudo dnf install -y wine winetricks libusb1
sudo dnf install -y gcc make pkgconfig libusb1-devel \
mingw32-gcc mingw32-binutils mingw32-headers mingw32-crt
# openSUSE
sudo zypper in -y wine winetricks libusb-1_0-0
sudo zypper in -y gcc make pkg-config libusb-1_0-devel mingw32-gcc
```
### Wine / App
- App: `Xgpro.exe`, `Xgpro_T76.exe`, or `Minipro.exe`
- The **shim DLL** must be placed alongside the app (see install)
---
## Install
### 1) Clone git repository
```bash
git clone https://github.com/radiomanV/TL866.git && cd TL866
```
### 2) Udev rules (recommended)
```bash
sudo cp ./udev/* /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger
```
This grants non-root access to the programmers.
### 3) Build the broker (native)
```bash
cd wine64
make broker # produces usb-broker
```
### 4) Build the Windows component
```bash
make windows # produces launcher.exe and shim.dll
```
#### A simple make will compile all three components above.
```bash
make
```
You can use the provided `shim.dll`, `launcher.exe`, and `usb-broker`.
`shim.dll` and `launcher.exe` do not need recompilation (theyre prebuilt natively), but
`usb-broker` may need to be rebuilt depending on your Linux distribution or architecture.
### 5) Place the shim, launcher and usb-broker next to the app
Copy the resulting files `usb-broker` `shim.dll` `launcher.exe` and (optionally) `run.sh`to the application directory.
Mark `usb-broker` and `run.sh` as executable:
```bash
chmod +x usb-broker run.sh
```
---
## Quick start
```
# Minimal, without any script:
cd /path/to/app/
./usb-broker --quiet & wine launcher.exe 2>/dev/null
# Or you can use the provided run.sh script, which can be customized via
# explicit environment variables:
BROKER_PORT=35866
APP_PATH="/path/to/launcher.exe"
TARGET_EXE="Xgpro.exe"
SHIM_DLL="shim.dll"
USB_BROKER_PATH="/path/to/xgecu-usb-broker"
WINECMD="wine64"
# This will start the broker (if needed) on the default port 35866 and
# run the launcher with the default Xgpro.exe name.
run.sh
# This will overide the default Xgpro.exe name:
TARGET_EXE=Minipro.exe ./run.sh
TARGET_EXE=Xgpro_T76.exe ./run.sh
# This will overide the default IPC port:
BROKER_PORT=35000 ./run.sh
```
- The script will:
- start the **broker** (if not running),
- launch Wine with the shimmed app,
- wire them together over the configured socket.
Works without a 32-bit prefix; you can use `~/.wine64`, default prefix or any Wine prefix you prefer.
The USB broker can also be run standalone for debugging. It accepts the following arguments:
```
--port <N> (listens on port N)
--quiet (reduced logging)
--no-exit (do not exit when there are no clients)
```
The USB broker supports multiple concurrent connections and can handle up to 64 USB devices.
Unless `--no-exit` is specified, the USB broker will automatically exit after a timeout.
---
## Configuration (env vars)
These are read by `run.sh` and/or the shim:
- `BROKER_PORT`: TCP port for shim-broker IPC (default: `35866`)
- `APP_PATH`: path to the Windows app launcher (optional)
- `TARGET_EXE`: the Windows binary to run (e.g. `Xgpro.exe`)
- `SHIM_DLL`: shim file name in the app folder (e.g. `shim.dll`)
- `USB_BROKER_PATH`: full path to the broker binary
- `WINECMD`: `wine` or `wine64` (default: `wine`)
- `TL_DEBUG`: set to `1` for verbose USB/protocol logs
---
## Troubleshooting
- **Device not found / permission denied**
- Recheck udev rules, unplug/replug, verify youre in the correct groups (e.g., `plugdev` on some distros).
- **Shim cannot find broker**
- Verify `BROKER_PORT` and that broker is running; try `USB_BROKER_PATH` absolute path.
- **Socket conflicts**
- Change `BROKER_PORT`.
- **Black toolbar when theme is active under Wine**
- Install native comctl32 v5.80 or disable the Wine theme.
```bash
winetricks comctl32 && wineboot
```
---
## Security & privacy
- The broker runs locally and exposes a local port (loopback only).
- No network access beyond the local IPC channel.
---

662
wine64/broker.c Normal file
View File

@@ -0,0 +1,662 @@
/*
* broker.c
* Minimal USB broker for shim.dll.
* The USB broker will redirect all RPC calls from the shim to the
* Linux USB subsystem using LibUSB calls.
* Created: September 10, 2025
* Author: radiomanV
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <fcntl.h>
#include <pthread.h>
#include <libusb-1.0/libusb.h>
#include "protocol.h"
typedef struct {
uint32_t id;
libusb_device_handle *h;
uint8_t ifnum;
char path[128];
} handle_entry_t;
typedef struct {
int sock;
int running;
libusb_hotplug_callback_handle cb;
} hotplug_sub_t;
static handle_entry_t libusb_map[64];
static pthread_mutex_t map_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t id_mutex = PTHREAD_MUTEX_INITIALIZER;
static int map_idx = 0;
static uint32_t map_next_id = 1;
static volatile int num_clients = 0;
static volatile int b_verbose = 1;
static volatile int b_sigint = 0;
#define LOG(fmt, ...) \
do { \
if (b_verbose) \
fprintf(stderr, "[broker] " fmt "\n", ##__VA_ARGS__); \
} while (0)
#define ERR(fmt, ...) fprintf(stderr, "[broker:ERR] " fmt "\n", ##__VA_ARGS__)
// Socket buffer send function
static int socket_send(int sock, const void *buf, int len)
{
const char *pbuf = (const char *)buf;
int done = 0;
while (done < len) {
int n = send(sock, pbuf + done, len - done, 0);
if (n <= 0)
return RPC_E_SEND_ERR;
done += n;
}
return EXIT_SUCCESS;
}
// Socket buffer receive function
static int socket_recv(int sock, void *buf, int len)
{
char *p = (char *)buf;
int rcvd = 0;
while (rcvd < len) {
int n = recv(sock, p + rcvd, len - rcvd, 0);
if (n <= 0)
return RPC_E_RECV_ERR;
rcvd += n;
}
return EXIT_SUCCESS;
}
// Socket reply function
static int send_reply(int s, uint16_t type, const void *payload, uint32_t plen)
{
rpc_hdr_t h = { .len = plen, .type = type, .version = PROTO_VERSION };
if (socket_send(s, &h, sizeof(h)))
return RPC_E_SEND_ERR;
if (plen && socket_send(s, payload, (int)plen))
return RPC_E_SEND_ERR;
return EXIT_SUCCESS;
}
// Helpers to store/read/delete a handle_entry
static int map_put(libusb_device_handle *h, uint8_t ifnum, uint32_t *out_id,
const char *path)
{
pthread_mutex_lock(&map_mutex);
if (map_idx >= (int)(sizeof(libusb_map) / sizeof(libusb_map[0]))) {
pthread_mutex_unlock(&map_mutex);
return EXIT_FAILURE;
}
uint32_t id;
pthread_mutex_lock(&id_mutex);
id = map_next_id++;
if (id == 0)
id = map_next_id++;
pthread_mutex_unlock(&id_mutex);
libusb_map[map_idx] =
(handle_entry_t){ .id = id, .h = h, .ifnum = ifnum };
strncpy(libusb_map[map_idx].path, path, sizeof(libusb_map[0].path));
map_idx++;
pthread_mutex_unlock(&map_mutex);
*out_id = id;
return EXIT_SUCCESS;
}
static handle_entry_t *map_get(uint32_t id)
{
for (int i = 0; i < map_idx; i++) {
if (libusb_map[i].id == id)
return &libusb_map[i];
}
return NULL;
}
static int map_del(uint32_t id)
{
pthread_mutex_lock(&map_mutex);
for (int i = 0; i < map_idx; i++) {
if (libusb_map[i].id == id) {
libusb_map[i] = libusb_map[map_idx - 1];
map_idx--;
pthread_mutex_unlock(&map_mutex);
return EXIT_SUCCESS;
}
}
pthread_mutex_unlock(&map_mutex);
return EXIT_FAILURE;
}
// LibUSB enumerate helper
static int usb_enum(dev_info_t **out_list, uint32_t *out_cnt)
{
libusb_device **list = NULL;
ssize_t n = libusb_get_device_list(NULL, &list);
if (n < 0)
return EXIT_FAILURE;
uint32_t cap = (uint32_t)(n > 512 ? 512 : n);
dev_info_t *arr = (dev_info_t *)calloc(cap, sizeof(dev_info_t));
if (!arr) {
libusb_free_device_list(list, 1);
return EXIT_FAILURE;
}
uint32_t cnt = 0;
for (ssize_t i = 0; i < n && cnt < cap; i++) {
libusb_device *dev = list[i];
struct libusb_device_descriptor dd;
if (libusb_get_device_descriptor(dev, &dd))
continue;
dev_info_t di = { 0 };
di.vid = dd.idVendor;
di.pid = dd.idProduct;
di.bus = libusb_get_bus_number(dev);
di.addr = libusb_get_device_address(dev);
di.ifnum = 0;
snprintf(di.path, sizeof(di.path), "usb:%u-%u", di.bus,
di.addr);
struct libusb_config_descriptor *cfg = NULL;
if (libusb_get_active_config_descriptor(dev, &cfg)) {
di.has_eps = 0;
memset(di.wMaxPacketSize, 0, sizeof(di.wMaxPacketSize));
} else {
di.has_eps = 1;
memset(di.wMaxPacketSize, 0, sizeof(di.wMaxPacketSize));
for (int i = 0; i < cfg->bNumInterfaces; i++) {
const struct libusb_interface *it =
&cfg->interface[i];
for (int a = 0; a < it->num_altsetting; a++) {
const struct libusb_interface_descriptor
*id = &it->altsetting[a];
for (int k = 0; k < id->bNumEndpoints;
k++) {
const struct libusb_endpoint_descriptor
*ep = &id->endpoint[k];
uint8_t num =
ep->bEndpointAddress &
0x0F;
if (num < 16)
di.wMaxPacketSize[num] =
ep->wMaxPacketSize;
}
}
}
}
libusb_free_config_descriptor(cfg);
arr[cnt++] = di;
}
libusb_free_device_list(list, 1);
*out_list = arr;
*out_cnt = cnt;
return EXIT_SUCCESS;
}
// LibUSB open_device RPC helper
static libusb_device_handle *open_device(const char *path, open_resp_t *resp)
{
if (resp) {
resp->status = RPC_E_OPEN_ERR;
resp->product[0] = '\0';
resp->speed = LIBUSB_SPEED_UNKNOWN;
resp->handle_id = 0;
}
int wa = 0, wb = 0;
if (!path)
return NULL;
if (strcmp(path, "usb:auto-first") == 0) {
wa = -1, wb = -1;
} else if (sscanf(path, "usb:%d-%d", &wb, &wa) != 2) {
return NULL;
}
libusb_device **list = NULL;
ssize_t n = libusb_get_device_list(NULL, &list);
if (n < 0)
return NULL;
libusb_device_handle *hnd = NULL;
for (ssize_t i = 0; i < n; i++) {
libusb_device *d = list[i];
if ((wb == -1 && wa == -1) ||
(libusb_get_bus_number(d) == wb &&
libusb_get_device_address(d) == wa)) {
if (!libusb_open(d, &hnd)) {
if (resp) {
libusb_device *dev =
libusb_get_device(hnd);
if (dev) {
resp->speed =
libusb_get_device_speed(
dev);
struct libusb_device_descriptor
dd;
if (libusb_get_device_descriptor(
dev, &dd) ==
LIBUSB_SUCCESS &&
dd.iProduct) {
unsigned char tmp[sizeof(
resp->product)];
int m = libusb_get_string_descriptor_ascii(
hnd,
dd.iProduct,
tmp,
sizeof(tmp) -
1);
if (m > 0) {
tmp[m] = 0;
strncpy(resp->product,
(const char
*)
tmp,
sizeof(resp->product) -
1);
resp->product
[sizeof(resp->product) -
1] = 0;
}
}
}
}
uint32_t id = 0;
if (map_put(hnd, 0, &id, path) == 0) {
if (resp) {
resp->handle_id = id;
}
int cfg = 0;
if (libusb_get_configuration(hnd,
&cfg) ==
LIBUSB_SUCCESS &&
cfg == 0) {
libusb_set_configuration(hnd,
1);
}
if (libusb_claim_interface(hnd, 0) ==
LIBUSB_SUCCESS)
resp->status = 0;
else
resp->status = RPC_E_OPEN_BUSY;
break;
} else {
libusb_close(hnd);
hnd = NULL;
}
}
if (wb != -1 || wa != -1)
break;
}
}
libusb_free_device_list(list, 1);
return hnd;
}
/* ------------ Client hotplug subscription socket notifier -------------*/
static int LIBUSB_CALL on_hotplug(libusb_context *ctx, libusb_device *dev,
libusb_hotplug_event event, void *user)
{
hotplug_sub_t *sub = (hotplug_sub_t *)user;
if (!sub || sub->sock < 0)
return 0;
struct libusb_device_descriptor desc;
if (libusb_get_device_descriptor(dev, &desc))
return 0;
hotplug_evt_t evt = { 0 };
evt.arrived = (event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED) ? 1 : 0;
evt.vid = desc.idVendor;
evt.pid = desc.idProduct;
evt.bus = libusb_get_bus_number(dev);
evt.addr = libusb_get_device_address(dev);
// Send reply to client
if (send_reply(sub->sock, MSG_HOTPLUG_EVT, &evt, sizeof(evt)) != 0) {
sub->running = 0;
}
return 0;
}
// LibUSB hotplug thread loop
static void *hotplug_loop(void *arg)
{
hotplug_sub_t *sub = (hotplug_sub_t *)arg;
struct timeval tv = { .tv_sec = 0, .tv_usec = 250000 };
while (sub->running) {
int rc =
libusb_handle_events_timeout_completed(NULL, &tv, NULL);
if (rc != 0)
usleep(1000);
char tmp;
int n = recv(sub->sock, &tmp, 1, MSG_PEEK | MSG_DONTWAIT);
if (n == 0) {
sub->running = 0;
break;
}
}
return NULL;
}
/* ---------- Client worker thread ---------------*/
static void *client_worker(void *arg)
{
//LOG("client connected (fd=%d)", sock);
int sock = (int)(intptr_t)arg;
for (;;) {
rpc_hdr_t hdr;
if (socket_recv(sock, &hdr, sizeof(hdr)))
break;
if (hdr.version != PROTO_VERSION) {
ERR("version mismatch: %u", hdr.version);
break;
}
uint8_t *request = NULL;
if (hdr.len) {
request = (uint8_t *)malloc(hdr.len);
if (!request)
break;
if (socket_recv(sock, request, (int)hdr.len)) {
free(request);
break;
}
}
uint8_t buf[1 << 16];
uint32_t len = 0;
switch (hdr.type) {
// Enumerate USB devices RPC
case MSG_ENUMERATE: {
dev_info_t *list = NULL;
uint32_t cnt = 0;
enum_resp_t *resp = NULL;
int rc = usb_enum(&list, &cnt);
LOG("Enumerate: %u USB devices found.", cnt);
if (rc == 0) {
len = sizeof(enum_resp_t) +
cnt * sizeof(dev_info_t);
resp = (enum_resp_t *)malloc(len);
if (!resp) {
if (list)
free(list);
break;
}
resp->status = 0;
resp->count = cnt;
memcpy((uint8_t *)resp + sizeof(*resp), list,
cnt * sizeof(dev_info_t));
free(list);
send_reply(sock, MSG_ENUMERATE, resp, len);
free(resp);
resp = NULL;
request = NULL;
continue;
} else {
enum_resp_t b = { .status = -1, .count = 0 };
send_reply(sock, MSG_ENUMERATE, &b, sizeof(b));
if (request)
free(request);
continue;
}
} break;
// Open device RPC
case MSG_OPEN: {
const open_req_t *rq = (const open_req_t *)request;
open_resp_t resp;
memset(&resp, 0, sizeof(resp));
resp.handle_id = 0;
resp.speed = 0;
resp.product[0] = '\0';
open_device(rq->path, &resp);
if (!resp.status) {
LOG("Open device: %s", rq->path);
}
send_reply(sock, MSG_OPEN, &resp, sizeof(resp));
} break;
// Close device RPC
case MSG_CLOSE: {
const close_req_t *rq = (const close_req_t *)request;
close_resp_t resp = { .status = -1 };
handle_entry_t *ent = map_get(rq->handle_id);
if (ent && ent->h) {
LOG("Close device: %s", ent->path);
libusb_release_interface(ent->h, ent->ifnum);
libusb_close(ent->h);
map_del(rq->handle_id);
resp.status = 0;
}
send_reply(sock, MSG_CLOSE, &resp, sizeof(resp));
} break;
// Bulk transfer RPC
case MSG_BULK: {
const bulk_req_t *rq = (const bulk_req_t *)request;
handle_entry_t *ent = map_get(rq->handle_id);
LOG("%s %s Bulk transfer: %u bytes", ent->path,
rq->ep & 0x80 ? "IN" : "OUT", rq->len);
int ep_in = ((rq->ep & 0x80) != 0);
int rc = -1, xfer = 0;
uint8_t *payload = NULL;
size_t pay_len = 0;
size_t hdr_len = sizeof(*rq);
if (!ep_in) {
payload = request + hdr_len;
pay_len = (hdr.len > hdr_len) ?
(hdr.len - hdr_len) :
0;
} else {
payload = buf + sizeof(bulk_resp_t);
pay_len = rq->len;
}
if (ent && ent->h) {
if (hdr.type == MSG_BULK) {
rc = libusb_bulk_transfer(
ent->h, rq->ep, payload,
(int)pay_len, &xfer,
rq->timeout_ms);
} else {
rc = libusb_interrupt_transfer(
ent->h, rq->ep, payload,
(int)pay_len, &xfer,
rq->timeout_ms);
}
}
bulk_resp_t *resp = (bulk_resp_t *)buf;
resp->status = rc;
resp->rx_len = (ep_in && rc == 0) ? (uint32_t)xfer : 0;
len = sizeof(*resp) + resp->rx_len;
send_reply(sock, hdr.type, buf, len);
} break;
// Cancel transfer RPC
case MSG_CANCEL: {
LOG("Cancel transfer");
cancel_resp_t resp = { .status = 0 };
send_reply(sock, MSG_CANCEL, &resp, sizeof(resp));
} break;
// Hotplug subscribe RPC
case MSG_HOTPLUG_SUB: {
LOG("Hotplug subcribe");
hotplug_sub_t sub = { .sock = sock, .running = 1 };
int rc = libusb_hotplug_register_callback(
NULL,
LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED |
LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,
0, LIBUSB_HOTPLUG_MATCH_ANY,
LIBUSB_HOTPLUG_MATCH_ANY,
LIBUSB_HOTPLUG_MATCH_ANY, on_hotplug, &sub,
&sub.cb);
if (rc != LIBUSB_SUCCESS) {
ERR("hotplug not available: %s",
libusb_error_name(rc));
} else {
pthread_t th;
// GCC atomic increment builtin
__sync_add_and_fetch(&num_clients, 1);
pthread_create(&th, NULL, hotplug_loop, &sub);
pthread_detach(th);
while (sub.running) {
char tmp;
int n = recv(sock, &tmp, 1,
MSG_PEEK | MSG_DONTWAIT);
if (n == 0) {
sub.running = 0;
break;
}
usleep(100000);
}
libusb_hotplug_deregister_callback(NULL, sub.cb);
if (request)
free(request);
// GCC atomic decrement builtin
__sync_sub_and_fetch(&num_clients, 1);
goto done;
}
} break;
default:
ERR("unknown msg type: 0x%04x", hdr.type);
break;
}
if (request)
free(request);
}
done:
close(sock);
//LOG("client disconnected (fd=%d)", sock);
return NULL;
}
// SIGINT handler
static void sigint_handler(int sig)
{
LOG("SIGINT");
b_sigint = 1;
}
/*-------------Main loop------------------------------*/
int main(int argc, char **argv)
{
int port = RPC_PORT_DEFAULT;
int no_exit = 0;
// Parse arguments
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--port") && i + 1 < argc) {
port = atoi(argv[++i]);
} else if (!strcmp(argv[i], "--quiet")) {
b_verbose = 0;
} else if (!strcmp(argv[i], "--no-exit")) {
no_exit = 1;
} else if (!strcmp(argv[i], "--help")) {
fprintf(stderr, "usb-broker options:\n");
fprintf(stderr, " --port N listen port (default %d)\n",
RPC_PORT_DEFAULT);
fprintf(stderr,
" --no-exit don't exit when no clients\n");
fprintf(stderr, " --quiet less logs\n");
return 0;
}
}
// Initialize LibUSB
if (libusb_init(NULL) != 0) {
ERR("libusb_init failed");
return EXIT_FAILURE;
}
// Initialize SIGINT handler
signal(SIGINT, sigint_handler);
// Initialize broker server
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
return EXIT_FAILURE;
}
int on = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind");
return EXIT_FAILURE;
}
if (listen(sock, 32) < 0) {
perror("listen");
return EXIT_FAILURE;
}
// Set the non-blocking flag
int flags = fcntl(sock, F_GETFL, 0) | O_NONBLOCK;
fcntl(sock, F_SETFL, flags);
LOG("listening on 127.0.0.1:%d", port);
// Enter client wait loop
int wait = 0;
do {
struct timeval tv = { .tv_sec = 3, .tv_usec = 0 };
fd_set rf;
FD_ZERO(&rf);
FD_SET(sock, &rf);
int rc = select(sock + 1, &rf, NULL, NULL, &tv);
if (rc < 0) {
if (errno == EINTR)
continue;
perror("select");
break;
}
// Accept and create a new client worker thread
if (rc > 0 && FD_ISSET(sock, &rf)) {
int cs = accept(sock, NULL, NULL);
if (cs >= 0) {
pthread_t th;
pthread_create(&th, NULL, client_worker,
(void *)(intptr_t)cs);
pthread_detach(th);
wait = 3;
}
}
if(wait) wait--;
} while ((num_clients || no_exit || wait) && !b_sigint);
close(sock);
libusb_exit(NULL);
return EXIT_SUCCESS;
}

190
wine64/launcher.c Normal file
View File

@@ -0,0 +1,190 @@
/*
* launcher.c
* Native x86 windows dll injector and launcher.
* Created: September 10, 2025
* Author: radiomanV
*/
#define UNICODE
#define _UNICODE
#include <windows.h>
#include <shlwapi.h>
#include <strsafe.h>
#include <stdio.h>
#include <wchar.h>
// Dll injector function
static BOOL inject_dll(HANDLE hProc, const wchar_t *dllPath)
{
SIZE_T size = (wcslen(dllPath) + 1) * sizeof(wchar_t), wr = 0;
LPVOID remote = VirtualAllocEx(
hProc, NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!remote)
return FALSE;
BOOL ok = WriteProcessMemory(hProc, remote, dllPath, size, &wr) &&
wr == size;
if (!ok) {
VirtualFreeEx(hProc, remote, 0, MEM_RELEASE);
return FALSE;
}
HMODULE hm = GetModuleHandleW(L"kernel32.dll");
FARPROC pLoadLib = hm ? GetProcAddress(hm, "LoadLibraryW") : NULL;
if (!pLoadLib) {
VirtualFreeEx(hProc, remote, 0, MEM_RELEASE);
return FALSE;
}
HANDLE thrd = CreateRemoteThread(hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)pLoadLib,
remote, 0, NULL);
if (!thrd) {
VirtualFreeEx(hProc, remote, 0, MEM_RELEASE);
return FALSE;
}
WaitForSingleObject(thrd, INFINITE);
DWORD ret = 0;
GetExitCodeThread(thrd, &ret);
CloseHandle(thrd);
VirtualFreeEx(hProc, remote, 0, MEM_RELEASE);
return ret != 0;
}
// Check file path
static int check_path(const wchar_t *base_dir, const wchar_t *arg, wchar_t *out,
size_t out_sz)
{
if (!arg || !*arg)
return 0;
if (!PathIsRelativeW(arg)) {
wcsncpy(out, arg, out_sz);
out[out_sz - 1] = L'\0';
} else {
if (!PathCombineW(out, base_dir, arg))
return 0;
}
DWORD att = GetFileAttributesW(out);
return (att != INVALID_FILE_ATTRIBUTES) &&
!(att & FILE_ATTRIBUTE_DIRECTORY);
}
// Main function
int wmain(int argc, wchar_t **argv)
{
const wchar_t *exe_arg = L"";
const wchar_t *shim_arg = L"shim.dll";
int forced = 0;
// Get the launcher's directory
wchar_t dir[MAX_PATH];
DWORD n = GetModuleFileNameW(NULL, dir, ARRAYSIZE(dir));
if (n && n < ARRAYSIZE(dir)) {
PathRemoveFileSpecW(dir);
}
// Parse command line
int pf = -1;
for (int i = 1; i < argc; i++) {
if (wcscmp(argv[i], L"--exe") == 0 && i + 1 < argc) {
exe_arg = argv[++i];
forced = 1;
continue;
}
if (wcscmp(argv[i], L"--shim") == 0 && i + 1 < argc) {
shim_arg = argv[++i];
continue;
}
if (wcscmp(argv[i], L"--") == 0) {
pf = i + 1;
break;
}
}
wchar_t exe_path[MAX_PATH], shim_path[MAX_PATH];
if (forced) {
if (!check_path(dir, exe_arg, exe_path, ARRAYSIZE(exe_path))) {
wprintf(L"[launcher] --exe path not found or invalid: \"%ls\"\n",
exe_arg);
return EXIT_FAILURE;
}
} else {
const wchar_t *progs[] = { L"Minipro.exe", L"Xgpro.exe",
L"Xgpro_T76.exe" };
int found = 0;
for (size_t i = 0; i < ARRAYSIZE(progs); ++i) {
if (check_path(dir, progs[i], exe_path,
ARRAYSIZE(exe_path))) {
exe_arg = progs[i];
found = 1;
break;
}
}
if (!found) {
wprintf(L"[launcher] --exe not specified and no known executable found.\n",
dir);
return EXIT_FAILURE;
}
}
// Resolve shim (accept absolute or relative)
if (!check_path(dir, shim_arg, shim_path, ARRAYSIZE(shim_path))) {
wprintf(L"[launcher] Shim not found: \"%ls\" (base: \"%ls\")\n",
shim_arg, dir);
return EXIT_FAILURE;
}
// Build the target process command line
wchar_t cmdline[4096];
int off = _snwprintf(cmdline, ARRAYSIZE(cmdline), L"\"%s\"", exe_path);
if (off < 0 || off >= (int)ARRAYSIZE(cmdline)) {
wprintf(L"[launcher] Command line too long (exe).\n");
return EXIT_FAILURE;
}
if (pf > 0) {
for (int i = pf; i < argc; ++i) {
int wrote = _snwprintf(cmdline + off,
ARRAYSIZE(cmdline) - off,
L" \"%ls\"", argv[i]);
if (wrote < 0 ||
off + wrote >= (int)ARRAYSIZE(cmdline)) {
wprintf(L"[launcher] Command line too long.\n");
return EXIT_FAILURE;
}
off += wrote;
}
}
// Create a suspended process
STARTUPINFOW si = { 0 };
si.cb = sizeof(si);
PROCESS_INFORMATION pinfo = { 0 };
if (!CreateProcessW(NULL, cmdline, NULL, NULL, FALSE,
CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT, NULL,
NULL, &si, &pinfo)) {
wprintf(L"[launcher] CreateProcessW failed, err=%lu\n",
GetLastError());
return EXIT_FAILURE;
}
// Inject our shim.dll into the target process
if (!inject_dll(pinfo.hProcess, shim_path)) {
wprintf(L"[launcher] DLL injection failed, err=%lu\n",
GetLastError());
TerminateProcess(pinfo.hProcess, 1);
CloseHandle(pinfo.hThread);
CloseHandle(pinfo.hProcess);
return EXIT_FAILURE;
}
// Resume process and wait for exit
ResumeThread(pinfo.hThread);
WaitForSingleObject(pinfo.hProcess, INFINITE);
DWORD ret = 0;
GetExitCodeProcess(pinfo.hProcess, &ret);
CloseHandle(pinfo.hThread);
CloseHandle(pinfo.hProcess);
return (int)ret;
}

BIN
wine64/launcher.exe Executable file

Binary file not shown.

141
wine64/protocol.h Normal file
View File

@@ -0,0 +1,141 @@
/*
* protocol.h
* USB broker protocol call definitions
* Created: September 10, 2025
* Author: radiomanV
*/
#pragma once
#include <stdint.h>
#if defined(_MSC_VER)
#define PACKED
#pragma pack(push, 1)
#else
#define PACKED __attribute__((__packed__))
#endif
#define PROTO_VERSION 2
#define RPC_PORT_DEFAULT 35866
// ---- MSG ----
enum msg_type {
MSG_ENUMERATE = 1,
MSG_OPEN,
MSG_CLOSE,
MSG_BULK,
MSG_CANCEL,
MSG_HOTPLUG_SUB,
MSG_HOTPLUG_EVT,
};
// ---- RPC HEADER ----
typedef struct PACKED {
uint32_t len;
uint16_t type;
uint16_t version;
} rpc_hdr_t;
// --- DEVICE INFO ---
typedef struct PACKED {
uint16_t vid, pid;
uint8_t bus, addr;
uint8_t ifnum;
uint8_t has_eps;
uint16_t wMaxPacketSize[16];
char path[128];
} dev_info_t;
// ---- ENUMERATE ----
typedef struct PACKED {
int32_t status;
uint32_t count;
dev_info_t devs[0];
} enum_resp_t;
// ---- OPEN ----
typedef struct PACKED {
char path[128];
} open_req_t;
typedef struct PACKED {
int32_t status;
uint32_t handle_id;
uint8_t speed;
char product[64];
} open_resp_t;
// ---- CLOSE ----
typedef struct PACKED {
uint32_t handle_id;
} close_req_t;
typedef struct PACKED {
int32_t status;
} close_resp_t;
// ---- BULK ----
typedef struct PACKED {
uint32_t handle_id;
uint8_t ep;
uint32_t timeout_ms;
uint32_t len;
} bulk_req_t;
typedef struct PACKED {
int32_t status;
uint32_t rx_len;
uint8_t data[0];
} bulk_resp_t;
// ---- CANCEL ----
typedef struct PACKED {
uint32_t handle_id;
} cancel_req_t;
typedef struct PACKED {
int32_t status;
} cancel_resp_t;
// ---- HOTPLUG ----
typedef struct PACKED {
uint16_t vid, pid;
} hotplug_sub_req_t;
typedef struct PACKED {
uint8_t arrived;
uint16_t vid, pid;
uint8_t bus, addr;
} hotplug_evt_t;
typedef enum rpc_err_t {
BR_E_WSA = -1,
BR_E_SOCK = -2,
BR_E_CONNECT_IMMEDIATE = -3,
BR_E_CONNECT_SOERR = -4,
BR_E_CONNECT_TIMEOUT = -5,
BRK_E_READY_TIMEOUT = -50,
BRK_E_READY_FAILED = -51,
BRK_E_WAIT_TIMEOUT = -52,
RPC_E_RESP_LEN_NULL = -100,
RPC_E_RESP_BUF_NULL = -101,
RPC_E_MKSOCK_FAIL = -102,
RPC_E_CONNECT_TIMEOUT = -103,
RPC_E_CONNECT_FAIL = -104,
RPC_E_SEND_HEADER = -105,
RPC_E_SEND_REQ = -106,
RPC_E_SEND_EXTRA = -107,
RPC_E_RECV_HEADER = -108,
RPC_E_PROTO_MISMATCH = -109,
RPC_E_RESP_TOO_LARGE = -110,
RPC_E_RECV_PAYLOAD = -111,
RPC_E_RECV_ERR = -112,
RPC_E_SEND_ERR = -113,
RPC_E_OPEN_BUSY = -114,
RPC_E_OPEN_ERR = -115,
} rpc_err_t;
#if defined(_MSC_VER)
#pragma pack(pop)
#endif
#undef PACKED

7
wine64/resource.h Normal file
View File

@@ -0,0 +1,7 @@
#pragma once
#define IDD_SHIM_ABOUTBOX 0xE100
#define IDS_SHIM_VERSION 0xE200
#define IDM_SHIM_ABOUT 0xE300
#define IDC_SHIM_LINK 0xE400
#define IDC_SHIM_APPICON 0xE401

38
wine64/resource.rc Normal file
View File

@@ -0,0 +1,38 @@
#include <windows.h>
#include "resource.h"
#define VER_MAJOR 1
#define VER_MINOR 0
#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)
#define IDI_APP 1
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
IDI_APP ICON "shim.ico"
STRINGTABLE
BEGIN
IDS_SHIM_VERSION, "v" STR(VER_MAJOR) "." STR(VER_MINOR)
END
// About dialog
IDD_SHIM_ABOUTBOX DIALOGEX 0, 0, 287, 118
STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About USB wrapper"
FONT 9, "MS Shell Dlg"
BEGIN
LTEXT "Minipro / Xgpro USB wrapper for Wine.", -1, 10, 10, 200, 10
LTEXT "Version:", -1, 10, 24, 40, 10
LTEXT "", IDS_SHIM_VERSION, 40, 24, 156, 10
LTEXT "(C) 2014-2025 radiomanV", -1, 10, 38, 266, 10
CONTROL "", -1, "Static", SS_BLACKFRAME | WS_CHILD | WS_VISIBLE, 10, 56, 270, 1
CONTROL "", IDC_SHIM_LINK, "Static", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 10, 70, 230, 24
ICON IDI_APP, IDC_SHIM_APPICON, 234, 21, 96, 96, WS_CHILD | WS_VISIBLE
DEFPUSHBUTTON "OK", IDOK, 228, 95, 50, 14
END

77
wine64/run.sh Executable file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -euo pipefail
PORT="${BROKER_PORT:-35866}"
DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
APP="${APP_PATH:-"$DIR/launcher.exe"}"
EXE="${TARGET_EXE:-Xgpro.exe}"
SHIM="${SHIM_DLL:-shim.dll}"
BROKER="${USB_BROKER_PATH:-"$DIR/usb-broker"}"
WINECMD="${WINECMD:-wine}"
have_nc() { command -v nc >/dev/null 2>&1; }
is_valid_port() {
case $1 in
''|*[!0-9]*) return 1
esac
[ "$1" -ge 1 ] && [ "$1" -le 65535 ]
}
port_ready() {
if ! is_valid_port "$PORT"; then
printf 'Invalid PORT="%s" (expected 1..65535)\n' "$PORT" >&2
exit 1
fi
if (exec 3<>"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null; then
exec 3>&- 3<&-
return 0
fi
if have_nc && nc -z 127.0.0.1 "$PORT" >/dev/null 2>&1; then
return 0
fi
return 1
}
wait_port() {
local timeout_sec="${1:-8}" i
for ((i=0; i<timeout_sec*10; i++)); do
if port_ready; then return 0; fi
sleep 0.1
done
return 1
}
start_broker() {
if port_ready; then
echo "usb-broker already running on 127.0.0.1:$PORT"
return 0
fi
if [[ ! -x "$BROKER" ]]; then
echo "ERROR: broker not executable: $BROKER" >&2
exit 1
fi
echo "Starting usb-broker: $BROKER --port $PORT"
chmod +x "$BROKER" || true
"$BROKER" --port "$PORT" --quiet &
local bpid=$!
echo "usb-broker PID=$bpid"
if ! wait_port 10; then
echo "ERROR: usb-broker did not open port $PORT in time" >&2
kill "$bpid" 2>/dev/null || true
exit 1
fi
}
run_app() {
cd "$DIR"
echo "Launching wine app: $EXE"
"$WINECMD" "$APP" --exe "$EXE" --shim "$SHIM" -- "$@" 2>/dev/null
}
start_broker
run_app "$@"

1973
wine64/shim.c Normal file

File diff suppressed because it is too large Load Diff

BIN
wine64/shim.dll Executable file

Binary file not shown.

BIN
wine64/shim.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
wine64/usb-broker Executable file

Binary file not shown.