DI Management Home > Calling a Windows Native DLL from GO

Calling a Windows Native DLL from GO


As an exercise, we wrote an interface to a Windows native DLL using GO. This page describes some lessons we learned. Then we look at a simple Windows DLL we wrote myTestDll.dll and its interface to Go, which demonstrates passing and receiving strings, byte arrays and integers.

Lessons Learned | A test Windows native DLL | String-handling Utilities | Downloads | Source code for myTestDll.dll | Contact us | Comments

You can see the Go interface to CryptoSys PQC at
CryptoSys PQC Interface for Go programmers and
https://github.com/davidi67/crsyspqc
We found the following article by Justen Walker very helpful indeed and we recommend it
Breaking all the rules: Using Go to call Windows API

Lessons Learned

Some lessons we learned over and above Justen's advice.

  1. A shortcut. You can replace
    var (
        kernel32DLL = syscall.NewLazyDLL("myTestDll.dll")
        proc = kernel32DLL.NewProc("MyFunc")
    )
    
    with
    var proc = syscall.NewLazyDLL("myTestDll.dll").NewProc("MyFunc")
    
  2. A DLL function with no parameters with signature MyFunc(void) must be called with at least one argument. That is, you must use proc.Call(0) instead of proc.Call(), which will raise an error. See MyVoidFunc below.
  3. There are no checks on the number of parameters passed. So it's up to you to make sure you pass the correct number of parameters.

    In general, if you get it wrong, your program will hang, as it presumably passes any old rubbish if you miss a parameter, so you immediately realise you have a problem.

    BUT if there is a subtle error, it might actually appear to finish without hanging. In that case it leaves a file named something like _debug_bin12345.exe in the current directory. If this happens, there is an error, just not severe enough to crash, so keep debugging.

  4. All parameters and return values are pointers to int. But 0 seems OK to use on its own. It looks like you can use 0 to pass a NULL pointer and also the zero integer value (see MyStringFunc).
    This pointer to int can be confusing. At first glance it looks like the return value is a simple integer
    var proc = syscall.NewLazyDLL("myTestDll.dll").NewProc("MyVoidFunc")
    ret, _, _ := proc.Call(0)
    fmt.Printf("MyVoidFunc returns %d\n", ret)  // 42
    
    This prints the result 42. But that's just the cleverness of the fmt.Printf function. If you try do some integer arithmetic with it, it may not do what you expect
    fmt.Println(-ret)  // 18446744073709551574
    You need to cast the result as an int first
    fmt.Println(int(-ret))  // -42

A test Windows native DLL

To demonstrate an interface with Go, we created a simple Windows native DLL myTestDll.dll with the following function signatures.

LONG __stdcall MyVoidFunc(void);
LONG __stdcall MyIntFunc(LONG n);
LONG __stdcall MyStringFunc(LPSTR szOutput, DWORD nOutChars, LPCSTR szInput, DWORD nOptions);
LONG __stdcall MyByteFunc(BYTE *lpOutput, DWORD nOutBytes, CONST BYTE *lpInput, DWORD nInBytes, DWORD nOptions);
LONG __stdcall MyUnicodeFunc(LPWSTR wsOutput, DWORD nOutChars, LPCWSTR wsInput, DWORD nOptions);
MyVoidFunc(void)
MyVoidFunc takes no arguments and always returns the integer 42 (the answer to the Great Question of Life, the Universe and Everything).
MyIntFunc(LONG n)
MyIntFunc takes an int32 argument n and returns the same number. n may be positive or negative.
MyStringFunc(LPSTR szOutput, DWORD nOutChars, LPCSTR szInput, DWORD nOptions)
MyStringFunc copies a null-terminated ANSI string szInput to the output buffer szOutput.

The function expects an output buffer of nOutChars plus one for the terminating zero. If called with a NULL szOutput or zero nOutChars it returns the required length excluding the terminating null. The nOptions parameter is for future use and is ignored. The function returns a negative number on error (e.g. output buffer too short). The intention is to call the function twice. Once to find the required output length, and then to call again with a properly allocated output buffer.

MyByteFunc(BYTE *lpOutput, DWORD nOutBytes, CONST BYTE *lpInput, DWORD nInBytes, DWORD nOptions);
MyByteFunc copies a byte array lpInput of length nInBytes to the output buffer lpOutput. It expects an output buffer of exactly nOutBytes bytes. It returns a negative number on error.
MyUnicodeFunc(LPWSTR wsOutput, DWORD nOutChars, LPCWSTR wsInput, DWORD nOptions)
MyUnicodeFunc copies a null-terminated input Unicode (UTF-16) string wsInput to the output buffer wsOutput. It expects an output buffer of nOutChars Unicode (wchar_t) characters plus one for the terminating zero. It returns a negative number on error.

These functions don't anything clever, but they demonstrate how to pass various types to a Windows DLL and receive the results. Real implementations like MyStringFunc and MyByteFunc would obviously be outputting something different than just a copy of the input.

NOTE: There is no difference in the functionality of a third-party native DLL like myTestDll.dll and a WinAPI DLL like kernel32.dll, they both work in an identical fashion. A third-party DLL can be located in the same directory as your main app - Windows will find it.

MyVoidFunc

var proc = syscall.NewLazyDLL("myTestDll.dll").NewProc("MyVoidFunc")
ret, _, _ := proc.Call(0)
// IMPORTANT: Must have at least one parameter, which is ignored but required by syscall.Call
// that is, use `proc.Call(0)` to call a function with no parameters, not `proc.Call()`
fmt.Printf("MyVoidFunc returns %d\n", ret)  // 42

MyIntFunc

var proc = syscall.NewLazyDLL("myTestDll.dll").NewProc("MyIntFunc")
ret, _, _ := proc.Call(0)  // Note we can pass a bare '0' for zero
fmt.Printf("MyIntFunc(0) returns %d\n", ret) // 0
n := 888
// But for a nonzero input, we must pass a uintptr
ret, _, _ = proc.Call(uintptr(n))
fmt.Printf("MyIntFunc(n) returns %d\n", ret) // 888
n = -123
// And for a negative integer input, we must first cast it to int32
ret, _, _ = proc.Call(uintptr(int32(n)))
// and then cast the negative return value
fmt.Printf("MyIntFunc(-n) returns %d\n", int32(ret))  // -123

MyStringFunc

Note the use of stringToCharPtr to pass the string input as a uintptr.

var proc = syscall.NewLazyDLL("myTestDll.dll").NewProc("MyStringFunc")
inputstr := "Hello World!"
fmt.Printf("MyStringFunc input is \"%s\"\n", inputstr)
proc = syscall.NewLazyDLL("myTestDll.dll").NewProc("MyStringFunc")
// 3a. Call the function with NULL output to find required length (excluding the terminating null)
nchars, _, _ := proc.Call(0, 0, // Note we can pass a bare '0' for NULL and for zero
    uintptr(unsafe.Pointer(stringToCharPtr(inputstr))),
    0)
fmt.Printf("MyStringFunc returns %d (expected 12)\n", nchars)
if int(nchars) < 0 {
    panic("MyStringFunc returned an error")
}
// 3b. Allocate buffer for ANSI string output as a *byte* array (uint8)
// plus add an extra one for the terminating null character
strbuf := make([]byte, nchars+1)
// 3c. Call the function again to get the output in the output buffer
nchars, _, _ = proc.Call(
    uintptr(unsafe.Pointer(&strbuf[0])),
    nchars,
    uintptr(unsafe.Pointer(stringToCharPtr(inputstr))),
    0)
// 3d. Trim the new output to remove the trailing null byte, and convert to a golang string type
outstr := string(strbuf[:nchars])
fmt.Printf("MyStringFunc output is \"%s\"\n", outstr)  // "Hello World!"

MyByteFunc

proc = syscall.NewLazyDLL("myTestDll.dll").NewProc("MyByteFunc")
inbytes := []byte{0xde, 0xad, 0xbe, 0xef}
fmt.Print("MyByteFunc input is (0x)", hex.EncodeToString(inbytes), "\n")
flags := 0xFEFF // nOptions flags
// 4a. Call the function with NULL output to find required length
nbytes, _, _ := proc.Call(0, 0,
    uintptr(unsafe.Pointer(&inbytes[0])),
    uintptr(len(inbytes)),
    uintptr(flags))
fmt.Printf("MyByteFunc returns %d (expected 4)\n", nbytes)
if int(nbytes) < 0 {
    panic("MyByteFunc returned an error")
}
// 4b. Allocate buffer for byte array output of exact required length
bbuf := make([]byte, nbytes)
// 4c. Call the function again to work on the input
nbytes, _, _ = proc.Call(
    uintptr(unsafe.Pointer(&bbuf[0])),
    nbytes,
    uintptr(unsafe.Pointer(&inbytes[0])),
    uintptr(len(inbytes)),
    uintptr(flags))
if int(nbytes) < 0 {
    panic("MyByteFunc returned an error")
}
// The byte array output is ready to work with
fmt.Print("MyByteFunc output is (0x)", hex.EncodeToString(bbuf), "\n")  // (0x)deadbeef

MyUnicodeFunc

Note the use of stringToUTF16Ptr to pass the Unicode string input as a uintptr, and utf16PtrToString to convert the output to a golang string type.

proc = syscall.NewLazyDLL("myTestDll.dll").NewProc("MyUnicodeFunc")
wstr := "Unicode: Привет 世界" // Hello World in Russian and Chinese
fmt.Printf("MyUnicodeFunc input is \"%s\"\n", wstr)
// 5a. Call the function with NULL output to find required length
flags = 888
nwchars, _, _ := proc.Call(0, 0,
    uintptr(unsafe.Pointer(stringToUTF16Ptr(wstr))),
    uintptr(flags))
fmt.Printf("MyUnicodeFunc returns %d (expected 18)\n", nwchars) // Expected 18
if int(nwchars) < 0 {
    panic("MyUnicodeFunc returned an error")
}
// 5b. Allocate buffer for UTF16 string output including the terminating null character
wstrbuf := make([]uint16, nwchars+1) // +1 for the null terminator
// 5c. Call the function again to work on the input string
_, _, _ = proc.Call(
    uintptr(unsafe.Pointer(&wstrbuf[0])),
    nwchars,
    uintptr(unsafe.Pointer(stringToUTF16Ptr(wstr))),
    uintptr(flags))
// 5d. Convert to a golang string type
woutstr := utf16PtrToString(&wstrbuf[0])
fmt.Printf("MyUnicodeFunc output is \"%s\"\n", woutstr)

String-handling Utilities

Credit for the functions stringToCharPtr and stringToUTF16Ptr to Justen Walker
from Convert Go strings to C-compatible strings for Windows API Calls

// StringToCharPtr converts a Go string into pointer to a null-terminated cstring.
func stringToCharPtr(str string) *uint8 {
    chars := append([]byte(str), 0) // null terminated
    return &chars[0]
}
// StringToUTF16Ptr converts a Go string into a pointer to a null-terminated UTF-16 wide string.
func stringToUTF16Ptr(str string) *uint16 {
    wchars := utf16.Encode([]rune(str + "\x00"))
    return &wchars[0]
}
// utf16PtrToString converts a pointer to a UTF-16 encoded string to a Go string.
func utf16PtrToString(ptr *uint16) string {
    if ptr == nil {
        return ""
    }
    var runes []rune
    for {
        r := *ptr
        if r == 0 {
            break
        }
        runes = append(runes, rune(r))
        ptr = (*uint16)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + unsafe.Sizeof(*ptr)))
    }
    return string(runes)
}

Downloads

Put the DLL in the same directory as the GO program, then run it
> go run mytestdll.go

The DLL is compiled for a 64-bit platform (x64) and is signed with our code-signing certificate.

Source code for myTestDll.dll

Contact us

To contact us, please send us a message. To make a comment see below.

This page first published 27 July 2025. Last updated 9 September 2025

Comments

   [Go to last comment] [Read our comments policy]
[Go to first comment]