Using syscall.SyscallN as replacement for syscall.Syscall

With no thanks to whoever organizes Go documentation, and in the hope that I can prevent someone else from wasting hours figuring out how to access Windows DLLs, this attempts to explain how to use the new syscall.SyscallN functionality to access custom DLLs on Windows (tested on 64-bit Win10 with a legacy 64-bit DLL written in C and compiled using Visual Studio 2019). For reference, as of June 28 2022 the underlying Go code for SyscallN is apparently only at https://github.com/golang/go/blob/master/src/syscall/dll_windows.go
The mkwinsyscall.go source may also be helpful.

  1. Make sure to import the necessary Go modules (this example assumes Go v1.18 +):
 import (
   	. . .
  	 "syscall"
 	  "unsafe"
 	  "golang.org/x/sys/windows"
)
  1. Load the DLL (this will Panic if DLL is not found):
    declare a lazy DLL reference and lazy pointer to the proc (procedure) address of the C function in the DLL:
var pLazyDll *syscall.LazyDLL
var pLazyProc *syscall.LazyProc
Then get an actual reference to the DLL (this assumes your DLL is in
Windows\System32 on 64 Windows - if not you'll need to look at Windows rules for locating DLLs):
pLazyDll = syscall.NewLazyDLL("name-of-dll-with-extension") 

For example:
syscall.NewLazyDLL("myDLL.dll")

  1. Get a reference to the procedure (this will Panic if the procedure is not found):
    pLazyProc = pLazyDll.NewProc(“name-of-proc-in-dll”) // use dumpbin or some other tool to list names
  2. Define a **Go** function to encapsulate calls to the C function in the DLL

YOU MUST KNOW THE TYPES OF ALL PARAMETERS AND THE RETURN TYPE OF THE C FUNCTION (the Procedure in DLL speak) !
As an example, for a **C** function that:

  • expects a 32-bit int and a C char buffer (const *char) as arguments (where the C function will fill the buffer with a NULL terminated C string)
  • returns a 32-bit int as an error code

The **Go** function wrapper (which uses syscall.SyscallN to call the C function in the DLL) would look like:

		func myGoFunction(p1 uint32, buf *byte) (err int32) {
			r0, _, _ := syscall.SyscallN(pLazyProc.Addr(), uintptr(p1), uintptr(unsafe.Pointer(buf)))
			err = int32(r0)
			return
		}

NOTE the Addr member of the pLazyProc structure contains the address of the function being called, and that ALL arguments must be cast to uintptr. The returned value in r0 must be cast to int32 to match the function signature. Refer to the source at the link above for more information about the values returned by syscall.SyscallN that are not used here.

  1. Prepare to call the function. Since in this example the **Go** function expects the DLL procedure to return a (NULL terminated) C string, a Go byte array must be created (allocated) by the caller to pass to the function (along with the uint32 argument value):
buffer := make([]byte, 256)	// size of buffer depends on size of strings that might be returned
var p1 uint32 = 42   // arbitrary for example
  1. Call the function and get results (this will Panic if buffer is too small):

    result := myGoFunction(p1, &buffer[0]) // NOTE use of address of first byte of buffer

    The result returned will be an int32.

    To access the (NULL terminated) string from the C buffer, convert it to a Go string using a
    function from the Go windows module:
    goString := windows.BytePtrToString(&buffer[0])

And there you have it.

Since the Edit button has mysteriously disappeared, note that in the description of the example C function strictly speaking the buffer parameter should be *char, NOT const *char since memory is changed (characters are put in the buffer) in the DLL.