Creating and Accessing DLL methods in GO

1,589 views
Skip to first unread message

Gladiators Squad

unread,
Sep 29, 2022, 4:58:02 AM9/29/22
to golang-nuts
Hi Everyone,

I'm trying to create go DLL for the below program using "go build -buildmode=c-shared -o calc.so calc.go"

package main
import "C"
func main() {}
//export SayHello
func SayHello(name string) {
fmt.Printf("Go says: Hello, %s!\n", name)
}
//export Add
func Add(num0, num1 int) int {
return num0 + num1
}

-----------------------------------------------------------------------------------------------------
I have written the below logic to access DLL methods,

import "C"
import (
   "fmt"
   "syscall"
   "unsafe"
)

var (
   Library      syscall.Handle
   IsInitialize = false
   data         = "World"
)



func Loaddll() {
   if Library == 0 {
      var error_val error
      fmt.Printf("creating handle of c-share Wrapper\n")
      Library, _ = syscall.LoadLibrary("calc.dll")
      if error_val != nil {
         fmt.Println(fmt.Sprintf("Error occured while loading dll - %s", error_val.Error()))
      }
      getDetails()
   }
}

func getDetails() {
   key_uintptr := getUintPtrOfString(data)
   helloMethod := getProc("SayHello")

   get_secret_result, _, _ := syscall.SyscallN(uintptr(helloMethod), key_uintptr)
   value := C.GoString((*C.char)(unsafe.Pointer(get_secret_result)))
   if get_secret_result != 0 {
      fmt.Println(fmt.Sprintf("Get code has given nonzero result\n %v", value))
   }

}

func getProc(funcname string) (result uintptr) {
   result, error_val := syscall.GetProcAddress(Library, funcname)
   if error_val != nil {
      fmt.Println(fmt.Sprintf("Error while getting proc %s", error_val.Error()))
   }
   return result
}

func getUintPtrOfString(s string) uintptr {
   byte_s := append([]byte(s), 0)
   return uintptr(unsafe.Pointer(&byte_s[0]))
}
----------------------------------------------------------------------------------------------------
I'm able to access the DLL method(Add) when we pass int as argument, but I'm getting errors while passing arguments as a string (Sayhello)..Here we are expecting the result as Go says: Hello,<some string>

Peter Galbavy

unread,
Sep 29, 2022, 5:33:23 AM9/29/22
to golang-nuts
On Linux at least - I have not tried building or using a Windows DLL, you have to accept C-style args and process them in the exported function signature:

e.g.

//export SendMail
func SendMail(n C.int, args **C.char) C.int {
    conf := parseArgs(n, args)
...

Here my parseArgs() func loops over the args and puts them in a map - which is what I want, but your requirement will be different.

Even if you are calling the function from Go code, I believe it still goes through the C ABI. I just followed https://pkg.go.dev/cmd/cgo

Peter

Peter Galbavy

unread,
Sep 29, 2022, 5:34:38 AM9/29/22
to golang-nuts
Oh, hang on, please ignore my last message. It's that was because the *caller* was defined that way - it's NOT a Go thing. Oops, my bad.

Peter

Brian Candler

unread,
Sep 29, 2022, 7:42:34 AM9/29/22
to golang-nuts
My first reaction is, do you *really* want to call a Go DLL from a Go main program? It seems to me like you will have two active copies of the Go runtime with their own garbage collectors etc.

Go "plugins" might be closer to what you need:

But there are a number of caveats:

Also consider whether you'd be better off with two separate processes communicating using gRPC or similar. Hashicorp have published a go-plugin library which takes this approach:

Gladiators Squad

unread,
Sep 30, 2022, 1:57:09 AM9/30/22
to golang-nuts
is go-plugin supported by Windows?...We want to create go dll and tey to access through go code

Brian Candler

unread,
Sep 30, 2022, 8:22:49 AM9/30/22
to golang-nuts
> is go-plugin supported by Windows?

If you mean go's own plugin system, then no: https://pkg.go.dev/plugin (sorry, I should have remembered that)

If you mean the go-plugin library from Hashicorp: I believe it should be fine.  It's used by Hashicorp Vault, and Vault supports Windows:

So I'd say it's very likely it does.  Besides, it uses gRPC, and that is generic across platforms.

> We want to create go dll and tey to access through go code

You haven't said *why* you want to do this.  There may be a better solution to what you're trying to achieve.

Gladiators Squad

unread,
Oct 3, 2022, 5:40:06 AM10/3/22
to Brian Candler, golang-nuts
We are trying to use a common code as a DLL which is shared by multiple services. The services need to support multiple platforms like Linux((amd64 and Armv7), Windows and FreeBSD. and we are using the following command to generate the DLL
 go build -buildmode=c-shared -o calc.so calc.go -- linux
go build -buildmode=c-shared -o calc.dll calc.go -- windows


--
You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-nuts/pk6vPUmx6lY/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/d46d6b49-ffe7-4262-8df7-3d4a13c7a448n%40googlegroups.com.

Brian Candler

unread,
Oct 3, 2022, 6:31:35 AM10/3/22
to golang-nuts
On Monday, 3 October 2022 at 10:40:06 UTC+1 squadglad...@gmail.com wrote:
We are trying to use a common code as a DLL which is shared by multiple services.

You've mixed a problem and solution statement together.

You want to share code between multiple services: if so, there are other solutions to this problem.  For example, you could provide it as a library which is compiled into those services (that's by far the simplest option).  Or you could put it in some other process that you call using gRPC.  Or you could spawn another process and talk to it over stdin/stdout using a protocol of your choice.

What's unclear to me is why you want the *dynamic linking* part of this. Is there some requirement which means you need to be able to swap out this shared code *at runtime*, without recompiling the underlying service which uses that code?

Gladiators Squad

unread,
Oct 4, 2022, 12:47:49 AM10/4/22
to Brian Candler, golang-nuts
We have some disk space restrictions and hence we do not want to include the common code as a library in the service. That will increase the size of each exe. We would like to keep the common code and service code independent of each other. Changes in one should not affect or cause recompilation of the other.

--
You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-nuts/pk6vPUmx6lY/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-nuts...@googlegroups.com.

Jason E. Aten

unread,
Oct 4, 2022, 11:56:05 AM10/4/22
to golang-nuts
@squadglad, it is certainly possible to compile go code into a windows DLL, and to use cgo. I've done it successfully.
You'll want to study up on cgo. There are strict rules for keeping go pointers and C pointers apart, and they
can be subtle. You'll need to be become well versed in the details discussed here https://pkg.go.dev/cmd/cgo .
Also you are back in the land of malloc() and free(), where it is easy to footgun yourself with a memory leak.

So its not the easiest design to make work. I would recommend generally against it, 
if you can avoid it. 

Each Go DLL (if there are multiple) will have its own copy of the Go runtime, which sets various signal handlers. The host
process, if Go based, will also have a runtime, and it will set various signal handlers.
Now we're getting into really dicey territory, which I happen to be working with just now,
since signal handling is going to be different on unix and windows, and you'll have to carefully figure out how to 
coordinate; this might not even be possible if you don't control the host process.
To say, "there be dragons" on the map, is apt.

I'll go into one particularly other subtle point to illustrate the hazards on Windows of Go DLLs:

The biggest gotcha that isn't well known is that your go DLL on windows will not behave like a normal windows DLL. Once it
is loaded, it cannot be unloaded. The go runtime starts background goroutines (threads), and there
is no way to safely stop these. Hence if you unload a c-shared Go DLL from a windows process,
it will crash the process when those threads try to run their code at addresses that no longer have
code after the DLL unload.

There is a partial workaround. Windows lets you pin a DLL to a process. Then, even if the
host process tries to unload the DLL, that request will be ignored.  If you control
the host process code, then you should simply never call FreeLibrary(). I was in
a situation some time back where I did not control the host process, and it would
try to unload my Go based DLL, and thus crash.  I had to pin the DLL to the process, so that
the FreeLibrary() call on it becomes a no-op. Something like this:

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved) {

  switch(ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
      LockLibraryIntoProcessMem(hModule, &g_Self);

      // On XP or later you can use GetModuleHandleEx with the 
      // GET_MODULE_HANDLE_EX_FLAG_PIN flag to prevent unloading of the DLL. 
      // Really strong. Can't be undone by double FreeLibrary().
      GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_PIN, &g_ModuleName[0], &g_Self2);

      break;
    case DLL_PROCESS_DETACH:
      break;
    }
  return TRUE;
}

// Lock this DLL in memory (since we'll crash if it gets unloaded due to
// running background threads suddenly finding their code pages gone).
// https://blogs.msmvps.com/vandooren/2006/10/09/preventing-a-dll-from-being-unloaded-by-the-app-that-uses-it/
HMODULE g_Self;
HMODULE g_Self2;
HHOOK   g_Hook;
TCHAR   g_ModuleName[1024];

// sets *LocalDllHandle == g_Self, and g_ModuleName, as side effects.
int LockLibraryIntoProcessMem(
                              HMODULE DllHandle,
                              HMODULE *LocalDllHandle)
{
  if(NULL == LocalDllHandle) {
    return ERROR_INVALID_PARAMETER;
  }
  *LocalDllHandle = NULL;
  if(0 == GetModuleFileName(
                            DllHandle,
                            g_ModuleName,
                            sizeof(g_ModuleName)/ sizeof(TCHAR)))
    return GetLastError();
  *LocalDllHandle = LoadLibrary(g_ModuleName);
  if(NULL == *LocalDllHandle)
    return GetLastError();
  return NO_ERROR;
}


Usually you would unload and re-load a DLL only to upgrade its code 
while keeping the host process running. That isn't going to be possible
when the DLL is pinned.  You have to restart the host process to
change the go code.  At this point, if you control the host code,
you might well conclude that it is easier to just compile the go
code into the host process, and skip the whole DLL thing. 

And you would be right.




On Monday, October 3, 2022 at 11:47:49 PM UTC-5 squadglad. wrote:
We have some disk space restrictions and hence we do not want to include the common code as a library in the service. That will increase the size of each exe. We would like to keep the common code and service code independent of each other. Changes in one should not affect or cause recompilation of the other.

On Mon, Oct 3, 2022 at 4:01 PM Brian Candler  wrote:

Brian Candler

unread,
Oct 5, 2022, 3:15:06 AM10/5/22
to golang-nuts
On Tuesday, 4 October 2022 at 05:47:49 UTC+1 squadglad...@gmail.com wrote:
We have some disk space restrictions and hence we do not want to include the common code as a library in the service. That will increase the size of each exe. We would like to keep the common code and service code independent of each other. Changes in one should not affect or cause recompilation of the other.

See also this other recent thread:

There are serious question marks around a Go main program calling a Go shared library or DLL, and you may end up debugging very difficult low-level issues.

If the objective is just to save some disk space, then I'd suggest no.  In fact, using the DLL may use *more* disk space overall, because of the size of the runtime it has to embed (even a "hello world" program takes nearly 2MB - tracked under #6853), whereas compiling a library into your main binary will share the existing runtime.

> We would like to keep the common code and service code independent of each other. Changes in one should not affect or cause recompilation of the other.

That's a different requirement.  You can achieve it using an interprocess communication mechanism like gRPC, and the Hashicorp go-plugin library wraps that pattern for you in a secure and easy-to-consume way.
Reply all
Reply to author
Forward
0 new messages