4 4
使用Go结合windows dll开发程序

Go程序有一个优点是很好地做到”跨平台”,一般开发的情况,我们使用内置的相关模块实现相关功能,通过 GOOS=操作系统代号 go build 就能编译出对应平台的二进制文件. 然后把二进制文件扔往对应的服务器无论是linux或windows, 基本都能正常运行.

但是Go也不是完全的跨平台,个别情况下并没有提供Windows下的相关方法,只能通过syscall包去调用Win库.

本文主要说说我们做跨平台的时候,针对windows的一些处理: 一种常见的方式是我们需要在go的代码里面使用个别的dll文件。

DLL是微软公司在微软视窗操作系统中实现共享函数库概念的一种实现方式。这些库函数的扩展名是.DLL、.OCX(包含ActiveX控制的库)或者.DRV(旧式的系统驱动程序)。

所谓动态链接(.DLL,Linux下.so文件),就是把一些经常会共用的代码(静态链接的OBJ程序库)制作成DLL,当可执行文件调用到DLL档内的函数时,Windows操作系统才会把DLL档加载内存内,DLL档本身的结构就是可执行档,当程序有需求时函数才进行链接。通过动态链接方式,内存浪费的情形将可大幅降低。静态链接库则是直接链接到可执行文件。

要解决空间浪费和更新困难这两个问题最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态地链接在一起。简单地讲,就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接(Dynamic Linking)的基本思想

要想了解Go的DLL编程,可以先阅读golang官方的相关资料 : WindowsDLLs

若要深入学习动态链接的相关背景和步骤,可以阅读《[程序员的自我修养]》

什么是syscall

在电脑中,系统调用(英语:system call),又称为系统呼叫,指运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供用户程序与操作系统之间的接口。大多数系统交互式操作需求在内核态执行。如设备IO操作或者进程间通信。”

实际上,你基本上做任何事情的时候,都需要系统调用。

  • 访问文件
  • 访问设备
  • 进程管理
  • 通讯
  • 时间 …

无论你是写C程序、写Go程序或者哪怕是写bash脚本,你实际上都会用到syscall

举一个简单的Go的例子 hello.go

package main
import "fmt"
func main() {
    fmt.Println("Hello, Tencent!")
}

如果我们在 Linux 上构建,并且使用 strace 的话,就可以看到发生了多少系统调用了:

$ go build hello.go
$ strace ./hello
execve("./hello", ["./hello"], [/* 23 vars */]) = 0
arch_prctl(ARCH_SET_FS, 0x52c008)       = 0
sched_getaffinity(0, 8192, [0])         = 8
mmap(0xc000000000, 65536, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc000000000
munmap(0xc000000000, 65536)             = 0
...
futex(0x52c0b0, FUTEX_WAIT, 0, NULL)    = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1eef3ac000
write(1, "Hello, Tencent!\n", 18Hello, Tencent!
)     = 18
futex(0x52ba58, FUTEX_WAKE, 1)          = 1
futex(0x52b990, FUTEX_WAKE, 1)          = 1
exit_group(0)                           = ?
+++ exited with 0 +++

可以关注一下最后的那个 write(), 那里就发生了syscall. 我们这里不关心相关底层细节,我们知道我们每天的编程其实都和syscall在互动

如何操作 syscall

syscall() saves CPU registers before making the system call, restores the registers upon return from the system call, and stores any error code returned by the system call in errno(3) if an error occurs.

就是说调用 syscall 之前先保存环境;syscall 返回之后,恢复环境;错误代码在 errno 中

以 sys_write 调用为例,系统调用号放到了 %rax 之中,文件的 fd 放到了 %rdi 中,要写入的 buf 放到了 %rsi 中,写入长度放到了 %rdx 之中。

当执行了 syscall() 后,开始进入 Trap,然后进入内核态,开始执行对应的系统调用的代码。而系统调用的返回值,会放到了 %rax 之中。

syscall vs. windows

举个例子,如果我们的程序平常运行在linux,为了做到程序的可可移植性, 我们会或多或少地调用winodws的相关API

下面我就通过一份代码,来说明Go在windows下运用dll,获取各个盘区的容量信息:

package main

import (
    "errors"
    "fmt"
    "syscall"
    "unsafe"
)

var kernel32 syscall.Handle

//初始化获取方法的引用
func init() {
    var err error
    kernel32, err = syscall.LoadLibrary("kernel32.dll")
    if err != nil {
        panic("获取方法应用错误")
    }

}

func getDriveNames() ([]string, error) {

    drives := []string{}

    LongPtr_DriveBuf := make([]byte, 256)

    getDrivesStringsEx, err := syscall.GetProcAddress(kernel32, "GetLogicalDriveStringsW")
    if err != nil {
        return nil, errors.New("call GetLogicalDriveStringsW fail")
    }

    //执行调用
    // 因为有2个参数,所以使用syscall就能放得下,最后的参数补0
    r, _, errno := syscall.Syscall(uintptr(getDrivesStringsEx), 2,
        uintptr(len(LongPtr_DriveBuf)),
        uintptr(unsafe.Pointer(&LongPtr_DriveBuf[0])), 0)

    if r != 0 {

        for _, v := range LongPtr_DriveBuf {
            if v < 65 || v > 90 {
                continue
            }
            //println(string(v))
            drives = append(drives, string(v)+":")
        }

    } else {
        return nil, errors.New(errno.Error())
    }

    return drives, nil
}

func getDiskGreeSpace(diskName string) {

    //将磁盘的名称转化为*UTF16
    diskNameUTF16Ptr, _ := syscall.UTF16PtrFromString(diskName)

    //使用长指针
    LongPtr_FreeBytesAvailable := int64(0)     //剩余空间
    LongPtr_TotalNumberOfBytes := int64(0)     //总空间
    LongPtr_TotalNumberOfFreeBytes := int64(0) //可用空间

    //获取方法的引用
    kernel32, err := syscall.LoadLibrary("kernel32.dll")
    if err != nil {
        panic("获取方法应用错误")
    }

    //释放方法引用
    defer syscall.FreeLibrary(kernel32)

    getDiskFreeSpaceEx, err := syscall.GetProcAddress(kernel32, "GetDiskFreeSpaceExW")
    if err != nil {
        panic("call GetZDiskFreeSpaceExW fail")
    }

    //执行调用
    // 因为有四个参数,所以使用syscall6才能放得下,最后两个参数补0
    r, _, errno := syscall.Syscall6(uintptr(getDiskFreeSpaceEx), 4,
        uintptr(unsafe.Pointer(diskNameUTF16Ptr)),
        uintptr(unsafe.Pointer(&LongPtr_FreeBytesAvailable)),
        uintptr(unsafe.Pointer(&LongPtr_TotalNumberOfBytes)),
        uintptr(unsafe.Pointer(&LongPtr_TotalNumberOfFreeBytes)),
        0, 0)

    if r != 0 {
        fmt.Printf(">>>> %s 的空间情况\n", diskName)
        fmt.Printf("剩余空间:%d G\n", LongPtr_FreeBytesAvailable/1024/1024/1024)
        fmt.Printf("用户可用空间:%d G\n", LongPtr_TotalNumberOfBytes/1024/1024/1024)
        fmt.Printf("剩余可用空间:%d G\n", LongPtr_TotalNumberOfFreeBytes/1024/1024/1024)

    } else {
        //此处的errno不是error接口,而是 type Errorno uintptr
        panic(errno)
    }
}

func main() {
    //释放方法引用
    defer syscall.FreeLibrary(kernel32)

    drives, err := getDriveNames()
    if err != nil {
        panic(err)
    }

    for _, d := range drives {
        //获取磁盘可用空间
        getDiskGreeSpace(d)
    }
}

为了可以丰富功能,做到更好的可移植性, 大家日常可以多看 MSDN文档 https://msdn.microsoft.com/en-us/library/windows/desktop/hh447209(v=vs.85)