goinvoke

package module
v1.3.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 30, 2023 License: MIT Imports: 10 Imported by: 0

README

goinvoke

Load DLLs and import functions with ease.

If all you need is an equivalent of LoadLibrary/dlopen and GetProcAddress/dlsym in Go, this library is a lot easier to work with than cgo. It does not require a C header to start with, and allows you to dynamically load different DLLs exposing the same set of functions.

Go Reference

Usage

Simply define a struct with attributes in the type of *windows.Proc or *windows.LazyProc, and call goinvoke.Unmarshal("path\\to\\file.dll", pointerToStruct). (Other OSes)

//go:build windows

package main

import (
	"errors"
	"fmt"
	"github.com/jamesits/goinvoke"
	"golang.org/x/sys/windows"
)

type Kernel32 struct {
	GetTickCount *windows.Proc

	// you can override the function name with a tag
	GetStartupInfo *windows.Proc `func:"GetStartupInfoW"`
}

func main() {
	k := Kernel32{}
	err := goinvoke.Unmarshal("kernel32.dll", &k)
	if err != nil {
		panic(err)
	}

	// a minimal example
	count, _, err := k.GetTickCount.Call()
	if !errors.Is(err, windows.ERROR_SUCCESS) {
		panic(err)
	}
	fmt.Printf("GetTickCount() = %d\n", count)

	// a more complete example
	startupInfo := windows.StartupInfo{}
	_, _, err = k.GetStartupInfo.Call(uintptr(unsafe.Pointer(&startupInfo)))
	if !errors.Is(err, windows.ERROR_SUCCESS) {
		panic(err)
	}
	lpTitle := windows.UTF16PtrToString(startupInfo.Title)
	fmt.Printf("lpTitle = %s\n", lpTitle)
}

For more examples of using this library, unmarshal_test.go is a good start point. If you need to define callback functions, see cgo_callback.go for an example.

Go: WindowsDLLs offers a great view of using the (*windows.Proc).Call() method.

Type Generator (Windows only)

Have a large DLL with a lot of functions and want to access all of them at once? Use our convenient invoker tool to generate the struct required! For example, if we want to call multiple functions in user32.dll, use the following commands to generate a "header":

go install github.com/jamesits/goinvoke/cmd/invoker@latest
invoker -dll "user32.dll" -generate

A file named user32_dll.go will be generated in the current directory with all the exports from that DLL. To use it in your code:

//go:build windows

package main

import (
	"github.com/jamesits/goinvoke"
)

func main() {
	var err error
	
	k := User32{}

	// either use the object method
	err = k.Unmarshal("user32.dll")
	// or use the global Unmarshal function
	err = goinvoke.Unmarshal("user32.dll", &k)
	
    // ...
}

In the future, when your DLL is updated with new exported functions, just re-generate the file:

go generate .

For advanced usage of this tool, run invoker -help.

Caveats

Relative Import (Windows only)

On Windows, due to security concerns, if the path is relative and only contains a base name (e.g. "kernel32.dll"), file lookup is limited to only %WINDIR%\System32. On platforms other than Windows, we always usedlopen(3) lookup order.

If you want to load a DLL packaged with your program (the DLL sits right beside your EXE, or under some sub-folder), the safe way is to get the directory where your program exists first:

package main

import (
	"github.com/jamesits/goinvoke"
	"github.com/jamesits/goinvoke/utils"
	"path/filepath"
)

type MyDll struct {
	// ...
}

func main() {
	var err error
	
	executableDir, err := utils.ExecutableDir()
	if err != nil {
		panic(err)
	}
	
	myDll := MyDll{}
	err = goinvoke.Unmarshal(filepath.Join(executableDir, /* optional */ "sub-folder", "MyDll.dll"), &myDll)
}

If you really want to load a DLL from your working directory, specify your intention explicitly with ".\\filename.dll". Loading a DLL from an arbitrary working directory might lead to serious security issues. DO NOT do this unless you know exactly what you are doing.

Cross Platform Usage

Since v1.3.0, goinvoke supports Linux, BSD and macOS. For example, on Linux you can:

//go:build linux

package main

import (
	"github.com/jamesits/goinvoke"
	"github.com/jamesits/goinvoke/utils"
)

type LibC struct {
	Puts *goinvoke.Proc `func:"puts"`
}

var libC LibC

func main() {
	err := goinvoke.Unmarshal("libc.so.6", &libC)
	if err != nil {
		panic(err)
	}

	_, _, _ = libC.Puts.Call(utils.StringToUintPtr("114514\n"))
}

For true cross-platform code, you can use goinvoke.FunctionPointer interface instead of *windows.Proc and *goinvoke.Proc.

Error Processing

The Unmarshal() method returns an error with type (*multierror.Error) if any of the following case happens:

  • DLL load fails (file does not exist, permission/ACL problem, WDAC/Code Integration policy, etc. )
  • The DLL file exists, but a function defined in the struct is not exported by that DLL

It always trys to fill as much as function pointers it can find, and will not be stopped by non-critical errors. So, depending on your use case, you can ignore certain errors reported by Unmarshal(), and use whether the struct field is nil as an indicator of exported function existence of your loaded DLL file.

If you really want to decode individual errors, use err.(*multierror.Error).Errors. There are some examples in unmarshal_test.go.

Importing Functions by Ordinal (Windows only)

Importing functions by ordinal is fully supported, just use *windows.Proc and add a ordinal tag. The ordinal tag, if exists, always overrides the func tag.

package main

import (
	"github.com/jamesits/goinvoke"
	"golang.org/x/sys/windows"
)

type shlwapi struct {
	// function definition compatible with Windows XP or earlier
	SHCreateMemStream *windows.Proc `ordinal:"12"`
}

func main() {
	var err error

	s := shlwapi{}
	err = goinvoke.Unmarshal("shlwapi.dll", &s)
	if err != nil {
		panic(err)
	}
	
	// ...
}

*windows.LazyProc does not support a ordinal tag.

Performance

syscall.Syscall is somewhat slower due to it allocating heap twice more than a cgo call (variable length arguments, and another copy inside syscall.Syscall()). There is a internal/benchmark package to compare the performance of *windows.Proc, *windows.LazyProc and cgo. Example result under Go 1.19.1:

goos: windows
goarch: amd64
pkg: github.com/jamesits/goinvoke/internal/benchmark
cpu: AMD Ryzen 9 5900X 12-Core Processor
BenchmarkSyscallIsDebuggerPresent
BenchmarkSyscallIsDebuggerPresent-24            30003824                38.32 ns/op
BenchmarkSyscallIsDebuggerPresentLazy
BenchmarkSyscallIsDebuggerPresentLazy-24        29269077                41.75 ns/op
BenchmarkCgoIsDebuggerPresent
BenchmarkCgoIsDebuggerPresent-24                41355494                30.92 ns/op
PASS

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrorNotFound        = errors.New("not found")
	ErrorUnmarshalFailed = errors.New("unmarshal failed")
)

Functions

func Unmarshal

func Unmarshal(path string, v any) error

Unmarshal loads the DLL into memory, then fills all struct fields with type *windows.LazyProc with exported functions.

Types

type DLL

type DLL struct {
	Name   string
	Handle uintptr
}

A DLL implements access to a single DLL.

func LoadDLL

func LoadDLL(name string) (*DLL, error)

LoadDLL loads the named DLL file into memory.

If name is not an absolute path and is not a known system DLL used by Go, Windows will search for the named DLL in many locations, causing potential DLL preloading attacks.

Use LazyDLL in golang.org/x/sys/windows for a secure way to load system DLLs.

func MustLoadDLL

func MustLoadDLL(name string) *DLL

MustLoadDLL is like LoadDLL but panics if load operation fails.

func (*DLL) FindProc

func (d *DLL) FindProc(name string) (proc *Proc, err error)

FindProc searches DLL d for procedure named name and returns *Proc if found. It returns an error if search fails.

func (*DLL) MustFindProc

func (d *DLL) MustFindProc(name string) *Proc

MustFindProc is like FindProc but panics if search fails.

func (*DLL) Release

func (d *DLL) Release() error

Release unloads DLL d from memory.

type FunctionPointer

type FunctionPointer interface {
	Addr() uintptr
	Call(...uintptr) (uintptr, uintptr, error)
}

type LazyDLL

type LazyDLL struct {
	Name   string
	System bool // unused
	// contains filtered or unexported fields
}

A LazyDLL implements access to a single DLL. It will delay the load of the DLL until the first call to its Handle method or to one of its LazyProc's Addr method.

LazyDLL is subject to the same DLL preloading attacks as documented on LoadDLL.

Use LazyDLL in golang.org/x/sys/windows for a secure way to load system DLLs.

func NewLazyDLL

func NewLazyDLL(name string) *LazyDLL

NewLazyDLL creates new LazyDLL associated with DLL file.

func NewLazySystemDLL

func NewLazySystemDLL(name string) *LazyDLL

NewLazySystemDLL is like NewLazyDLL, but will only search Windows System directory for the DLL if name is a base name (like "advapi32.dll").

func (*LazyDLL) Handle

func (d *LazyDLL) Handle() uintptr

Handle returns d's module handle.

func (*LazyDLL) Load

func (d *LazyDLL) Load() error

Load loads DLL file d.Name into memory. It returns an error if fails. Load will not try to load DLL, if it is already loaded into memory.

func (*LazyDLL) NewProc

func (d *LazyDLL) NewProc(name string) *LazyProc

NewProc returns a LazyProc for accessing the named procedure in the DLL d.

type LazyProc

type LazyProc struct {
	Name string
	// contains filtered or unexported fields
}

A LazyProc implements access to a procedure inside a LazyDLL. It delays the lookup until the Addr, Call, or Find method is called.

func (*LazyProc) Addr

func (p *LazyProc) Addr() uintptr

Addr returns the address of the procedure represented by p. The return value can be passed to Syscall to run the procedure.

func (*LazyProc) Call

func (p *LazyProc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error)

Call executes procedure p with arguments a. See the documentation of Proc.Call for more information.

func (*LazyProc) Find

func (p *LazyProc) Find() error

Find searches DLL for procedure named p.Name. It returns an error if search fails. Find will not search procedure, if it is already found and loaded into memory.

type Proc

type Proc struct {
	Dll  *DLL
	Name string
	// contains filtered or unexported fields
}

A Proc implements access to a procedure inside a DLL.

func (*Proc) Addr

func (p *Proc) Addr() uintptr

Addr returns the address of the procedure represented by p. The return value can be passed to Syscall to run the procedure.

func (*Proc) Call

func (p *Proc) Call(a ...uintptr) (uintptr, uintptr, error)

Call executes procedure p with arguments a.

The returned error is always non-nil, constructed from the result of GetLastError. Callers must inspect the primary return value to decide whether an error occurred (according to the semantics of the specific function being called) before consulting the error. The error always has type syscall.Errno.

On amd64, Call can pass and return floating-point values. To pass an argument x with C type "float", use uintptr(math.Float32bits(x)). To pass an argument with C type "double", use uintptr(math.Float64bits(x)). Floating-point return values are returned in r2. The return value for C type "float" is math.Float32frombits(uint32(r2)). For C type "double", it is math.Float64frombits(uint64(r2)).

Directories

Path Synopsis
cmd
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL