feat(dll): 自 go-hua 迁入 Windows 系统 API 封装

- 互斥体防多开/ShellExecute/窗口类名与标题查询/顶层窗口目录, 新增依赖 lxn/win
- 仅 Windows, 附测试、examples/dll 例程与包 README
This commit is contained in:
2026-09-20 12:29:36 +08:00
parent a4920a6a8e
commit ef7cd3e3c4
7 changed files with 390 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
# dll
Windows 系统 API 封装(基于 `lxn/win`):互斥体防多开、`ShellExecute` 拉起程序、
窗口类名/标题查询、顶层窗口路径定位。
> 迁移自 go-hua/dll,代码保持原样。**仅 Windows 可用**(依赖 user32/kernel32)。
## 用法
```go
import "git.zeroonesoft.cn/golib/zogo/dll"
// 互斥体防多开
h, err := dll.CreateMutex(false, "Global\\MyAppMutex") // 已存在则 err
defer dll.ReleaseMutex(h)
// 拉起外部程序(hide=true 隐藏窗口)
ok := dll.ShellExecute("", "notepad.exe", "C:\\a.txt", false)
// 窗口查询
title, _ := dll.GetWindowTextString(hwnd)
cls, _ := dll.GetWindowClassString(hwnd)
hwnd2 := dll.GetWindowFromClassName(0, "Chrome_WidgetWin_1")
// 顶层窗口(主程序)所在目录
dir, err := dll.GetTopFolderPath()
```
完整可运行例程:[examples/dll/main.go](../examples/dll/main.go)
## 注意
- 构建目标必须含 Windows(`GOOS=windows`);其他平台编译请将该包的使用隔离在
带 build tag 的文件里。
- `CreateMutex` 的 err 判断用 `windows.ERROR_ALREADY_EXISTS` 语义,见测试用例。
+71
View File
@@ -0,0 +1,71 @@
//go:build windows
package dll
import (
"fmt"
"syscall"
"testing"
)
func TestCreateMutex(t *testing.T) {
name := fmt.Sprintf("go-hua-test-mutex-%d", syscall.Getpid())
handle, err := CreateMutex(true, name)
if err != nil {
t.Fatalf("CreateMutex: %v", err)
}
if handle == 0 {
t.Fatal("CreateMutex 句柄不应为 0")
}
syscall.CloseHandle(handle)
// 重复创建同名互斥体(不持有)应成功且不报错
handle2, err := CreateMutex(false, name)
if err != nil {
t.Errorf("重复 CreateMutex 不应报错: %v", err)
}
if handle2 != 0 {
syscall.CloseHandle(handle2)
}
}
func TestReleaseMutex(t *testing.T) {
pid := syscall.Getpid()
// 创建并持有 → 释放应成功(ReleaseMutex 传句柄值)
handle, err := CreateMutex(true, fmt.Sprintf("go-hua-release-mutex-%d", pid))
if err != nil {
t.Fatalf("CreateMutex: %v", err)
}
defer syscall.CloseHandle(handle)
if !ReleaseMutex(handle) {
t.Error("持有者 ReleaseMutex 应返回 true")
}
// 另一个未持有的互斥体 → 释放应失败
// 注意:同名互斥体重复创建会返回 183(Windows 正常行为),故使用不同名称
handle2, err := CreateMutex(false, fmt.Sprintf("go-hua-release-mutex2-%d", pid))
if err != nil {
t.Fatalf("CreateMutex: %v", err)
}
defer syscall.CloseHandle(handle2)
if ReleaseMutex(handle2) {
t.Error("未持有者 ReleaseMutex 应返回 false")
}
}
func TestReleaseMutexInvalidHandle(t *testing.T) {
// 无效句柄释放应失败且不崩溃
if ReleaseMutex(0) {
t.Error("ReleaseMutex(0) 应返回 false")
}
}
func TestOpenMutexNonExist(t *testing.T) {
_, err := OpenMutex(false, "go-hua-not-exist-mutex-xyz")
if err == nil {
t.Error("打开不存在的互斥体应返回错误")
}
}
+21
View File
@@ -0,0 +1,21 @@
// Package dll 封装 Windows 系统 API(互斥体防多开、ShellExecute、窗口操作等)。
package dll
import (
"github.com/lxn/win"
"syscall"
)
// ShellExecute 调用系统 ShellExecute 打开文件/网页/程序
// dir 工作目录;file 目标(文件路径、URL 或程序);args 启动参数;hide 是否隐藏窗口
func ShellExecute(dir string, file string, args string, hide bool) bool {
lpOperation, _ := syscall.UTF16PtrFromString("open")
lpFile, _ := syscall.UTF16PtrFromString(file)
lpCwd, _ := syscall.UTF16PtrFromString(dir)
lpArgs, _ := syscall.UTF16PtrFromString(args)
showCmd := win.SW_SHOWNORMAL
if hide {
showCmd = win.SW_HIDE
}
return win.ShellExecute(0, lpOperation, lpFile, lpArgs, lpCwd, showCmd)
}
+18
View File
@@ -0,0 +1,18 @@
package dll
import "syscall"
// kernel32.dll 互斥体相关 API 函数指针
var (
kernel32Dll *syscall.DLL
procCreateMutexW *syscall.Proc
procOpenMutexW *syscall.Proc
procReleaseMutex *syscall.Proc
)
func init() {
kernel32Dll = syscall.MustLoadDLL("kernel32.dll")
procCreateMutexW = kernel32Dll.MustFindProc("CreateMutexW")
procOpenMutexW = kernel32Dll.MustFindProc("OpenMutexW")
procReleaseMutex = kernel32Dll.MustFindProc("ReleaseMutex")
}
+76
View File
@@ -0,0 +1,76 @@
package dll
import (
"fmt"
"github.com/lxn/win"
"syscall"
"unsafe"
)
// CreateMutex 创建命名互斥体(常用于防止程序多开)
// bInitialOwner 为是否立即持有;互斥体已存在时返回错误(错误码 183)
func CreateMutex(bInitialOwner bool, mutexName string) (syscall.Handle, error) {
// 互斥体名需转为 UTF-16 指针(LPCWSTR),不能直接传 Go string 的地址
namePtr, nameErr := syscall.UTF16PtrFromString(mutexName)
if nameErr != nil {
return 0, nameErr
}
handle, _, err := syscall.SyscallN(procCreateMutexW.Addr(), 0, uintptr(win.BoolToBOOL(bInitialOwner)), uintptr(unsafe.Pointer(namePtr)))
if err != 0 {
println(err.Error())
lastError := win.GetLastError()
fmt.Println("lastError", lastError)
if lastError == 183 {
fmt.Println("Mutex 已经存在")
}
return 0, err
}
//defer syscall.CloseHandle(syscall.Handle(m))
if syscall.Handle(handle) != 0 {
lastError := win.GetLastError()
if lastError == 183 {
//fmt.Println("Mutex 已经存在")
}
}
// do something with the mutex
return syscall.Handle(handle), nil
}
// MUTANT_ALL_ACCESS 互斥体的完全访问权限
const MUTANT_ALL_ACCESS = 2031617
// OpenMutex 打开已存在的命名互斥体
func OpenMutex(bInheritHandle bool, mutexName string) (syscall.Handle, error) {
desiredAccess := MUTANT_ALL_ACCESS
handle, _, err := syscall.SyscallN(procOpenMutexW.Addr(), uintptr(desiredAccess), uintptr(win.BoolToBOOL(bInheritHandle)), uintptr(unsafe.Pointer(&mutexName)))
if err != 0 {
return 0, err
}
defer syscall.CloseHandle(syscall.Handle(handle))
if syscall.Handle(handle) == 0 {
e2 := win.GetLastError()
if e2 == 183 {
fmt.Println("打开失败")
}
fmt.Println(e2)
} // do something with the mutex
fmt.Println("Mutex open", syscall.Handle(handle))
return syscall.Handle(handle), nil
}
// ReleaseMutex 释放互斥体持有权
func ReleaseMutex(handle syscall.Handle) bool {
// 传句柄值本身,而非句柄变量的地址
ret, _, e1 := syscall.SyscallN(procReleaseMutex.Addr(), uintptr(handle))
if e1 != 0 {
println(e1.Error())
return false
}
return ret != 0
}
+130
View File
@@ -0,0 +1,130 @@
//go:build (windows && amd64) || (windows && 386)
package dll
import (
"github.com/lxn/win"
"syscall"
"unsafe"
)
var (
user32Dll *syscall.DLL
procGetWindowTextW *syscall.Proc
procGetWindowClassW *syscall.Proc
procFindWindowExW *syscall.Proc
)
func init() {
user32Dll = syscall.MustLoadDLL("user32.dll")
procGetWindowTextW = user32Dll.MustFindProc("GetWindowTextW")
procGetWindowClassW = user32Dll.MustFindProc("GetClassNameW")
procFindWindowExW = user32Dll.MustFindProc("FindWindowExW")
}
// GetWindowText 获取窗口标题(user32 GetWindowTextW 原生调用)
func GetWindowText(hWnd win.HWND, str *uint16, maxCount int32) (len int32, err error) {
//r0, _, e1 := procGetWindowTextW.Call(uintptr(hWnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount))
r0, _, e1 := syscall.Syscall(procGetWindowTextW.Addr(), 3, uintptr(hWnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount))
len = int32(r0)
if len == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
// GetWindowTextString 获取窗口标题并转为 string
func GetWindowTextString(hWnd win.HWND) (string, error) {
buf := make([]uint16, 255)
_, err := GetWindowText(hWnd, &buf[0], int32(len(buf)))
if err != nil {
return "", err
}
windowsTitle := syscall.UTF16ToString(buf)
return windowsTitle, nil
}
// GetTopFolderPath 获取资源管理器当前打开的顶层路径
func GetTopFolderPath() (string, error) {
//lName := uint16(0)
hWnd := win.FindWindow(syscall.StringToUTF16Ptr("CabinetWClass"), nil)
if hWnd == 0 {
return "", nil
}
hWnd = GetWindowFromClassName(hWnd, "Breadcrumb Parent")
if hWnd == 0 {
return "", nil
}
hWnd = FindWindowExW(hWnd, 0, syscall.StringToUTF16Ptr("ToolbarWindow32"), nil)
if hWnd == 0 {
return "", nil
}
buf := make([]uint16, 255)
_, err := GetWindowText(hWnd, &buf[0], int32(len(buf)))
if err != nil {
return "", err
}
title := syscall.UTF16ToString(buf)
return title, nil
}
// FindWindowExW 查找子窗口(user32 FindWindowExW 原生调用)
func FindWindowExW(parent win.HWND, child win.HWND, lpClassName, lpWindowName *uint16) win.HWND {
ret, _, _ := procFindWindowExW.Call(uintptr(parent), uintptr(child),
uintptr(unsafe.Pointer(lpClassName)),
uintptr(unsafe.Pointer(lpWindowName)),
0)
return win.HWND(ret)
}
// GetWindowFromClassName 使用类名获取窗口句柄 递归
func GetWindowFromClassName(parentWnd win.HWND, className string) win.HWND {
// 寻找顶层窗口
hWnd := FindWindowExW(parentWnd, 0, nil, nil)
for hWnd != 0 {
// 获取窗口类名
name, _ := GetWindowClassString(hWnd)
if name == className {
return hWnd
}
// 递归查找
phWnd := GetWindowFromClassName(hWnd, className)
if phWnd != 0 {
return phWnd
}
// 查找下一子窗口
hWnd = FindWindowExW(parentWnd, hWnd, nil, nil)
}
return 0
}
// GetWindowClassString 取窗口类名 string
func GetWindowClassString(hWnd win.HWND) (string, error) {
buf := make([]uint16, 255)
_, err := GetWindowClass(hWnd, &buf[0], int32(len(buf)))
if err != nil {
return "", err
}
return syscall.UTF16ToString(buf), nil
}
// GetWindowClass 取窗口类名 API
func GetWindowClass(hWnd win.HWND, str *uint16, maxCount int32) (len int32, err error) {
//r0, _, e1 := procGetWindowTextW.Call(uintptr(hWnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount))
r0, _, e1 := syscall.Syscall(procGetWindowClassW.Addr(), 3, uintptr(hWnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount))
len = int32(r0)
if len == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
+39
View File
@@ -0,0 +1,39 @@
// dll 包示例:Windows API 调用(互斥体防多开、ShellExecute、创建进程、窗口文本)
package main
import (
"fmt"
"syscall"
"git.zeroonesoft.cn/golib/zogo/dll"
)
func main() {
// 创建命名互斥体(常用于防止程序多开)
handle, err := dll.CreateMutex(false, "go-hua-example-mutex")
if err != nil {
fmt.Println("CreateMutex 失败(可能已存在):", err)
} else {
fmt.Println("CreateMutex 成功, 句柄:", handle)
}
// 打开已存在的互斥体
h2, err := dll.OpenMutex(false, "go-hua-example-mutex")
fmt.Println("OpenMutex:", h2, "错误:", err)
if handle != 0 {
fmt.Println("ReleaseMutex:", dll.ReleaseMutex(handle))
_ = syscall.CloseHandle(handle)
}
// 以下功能会产生可见窗口,按需取消注释:
//
// 用系统默认程序打开网页/文件:
// dll.ShellExecute("", "https://github.com", "", false)
//
// 创建进程(打开记事本):
// dll.CreateProcess()
//
// 获取资源管理器当前打开的顶层路径:
// path, err := dll.GetTopFolderPath()
}