Reputation: 987
I have a dynamic library from which I need to call a procedure in my Go program, but I can't seem to get it right. I have another program written in C# which uses this DLL and according to dnSpy dllimport compiles to:
[DllImport("Library.dll", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
public static extern void Calc(double[] Input, [MarshalAs(UnmanagedType.VBByRefStr)] ref string Pattern, [MarshalAs(UnmanagedType.VBByRefStr)] ref string Database_path, double[] Output);
So basing on this I tried to call it in my code like so:
func main() {
lib := syscall.NewLazyDLL("Library.dll")
proc := lib.NewProc("Calc")
input := [28]float64{0.741, 0.585, 2, 12, 1, 1, 1, 101325, 2500, 3, 20, 17.73, 17.11, 45, 1, 0, 80, 60, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0}
output := [20]float64{}
tubePattern := marshalAnsi("P8-16G-E145/028")
basePath, _ := filepath.Abs(".")
databasePath := marshalAnsi(basePath)
a1 := uintptr(unsafe.Pointer(&input))
a2 := uintptr(unsafe.Pointer(tubePattern))
a3 := uintptr(unsafe.Pointer(databasePath))
a4 := uintptr(unsafe.Pointer(&output))
ret, _, _ := proc.Call(a1, a2, a3, a4)
log.Println(ret)
log.Println(output)
}
func marshalAnsi(input string) *[]byte {
var b bytes.Buffer
writer := transform.NewWriter(&b, charmap.Windows1252.NewEncoder())
writer.Write([]byte(input))
writer.Close()
output := b.Bytes()
return &output
}
This results in unhandled delphi exception:
Exception 0xeedfade 0x31392774 0x32db04e4 0x7677ddc2
PC=0x7677ddc2
syscall.Syscall6(0x314af7d0, 0x4, 0x10ee7eb8, 0x10ed21a0, 0x10ed21d0, 0x10ee7d78, 0x0, 0x0, 0x0, 0x0, ...)
C:/Go32/src/runtime/syscall_windows.go:184 +0xcf
syscall.(*Proc).Call(0x10ed21e0, 0x10ed85e0, 0x4, 0x4, 0x10, 0x49bee0, 0x533801, 0x10ed85e0)
C:/Go32/src/syscall/dll_windows.go:152 +0x32e
syscall.(*LazyProc).Call(0x10ecb9c0, 0x10ed85e0, 0x4, 0x4, 0x0, 0x0, 0x4c6d70, 0x5491a0)
C:/Go32/src/syscall/dll_windows.go:302 +0x48
main.main()
C:/Users/Krzysztof Kowalczyk/go/src/danpoltherm.pl/dllloader/main.go:32 +0x2c0
eax 0x19f3f0
ebx 0x31392774
ecx 0x7
edx 0x0
edi 0x10ee7d78
esi 0x31392774
ebp 0x19f448
esp 0x19f3f0
eip 0x7677ddc2
eflags 0x216
cs 0x23
fs 0x53
gs 0x2b
I believe that problem might lie in the way I pass these strings to procedure, but I can't see what my code does different than that C# application.
Library have a few other procedures, I can call DLL_Version
with no problems like so:
lib := syscall.NewLazyDLL("Library.dll")
verProc := lib.NewProc("DLL_Version")
var version float64
verProc.Call(uintptr(unsafe.Pointer(&version)))
log.Printf("DLL version: %f\n", version)
Outputs:
2018/09/10 09:13:11 DLL version: 1.250000
EDIT: I believe that I found out what causes the exception. It seems that when called from my code, 2nd and 3rd parameters for this procedure, which should be pointers to string buffers, are instead pointers to pointers on these buffers, if I break on the beginning of procedure and patch this manually, code runs and terminates correctly. I still did not found a reason why this happens, and how to fix it though.
EDIT2: Apparently my previous assessment of this case was invalid. I was able to stop program from throwing exception, but I analyzed assembly and it turned out it was only due to program exiting before this exception was thrown, and instead of exception I got normal error code about invalid string.
While I'm not really good at understanding disassembled code, it seems that problem might indeed be caused by calling convention mismatch. At the beginning of procedure code, when called from working program EAX, EBX, ECX and EDX registers are empty, meanwhile when called from my code, only EAX and ECX are empty, EDX holds something but EBX points to Calc procedure pointer in my executable. Later in the DLL there is a part which checks if EBX is empty and throws exception if not.
EDIT 3: Well, that seems to also be unrelated, I'm aware that there is too little information for anyone to be able to make any guess as to what is going on. So I'll try to track where this exception comes from exactly and come back to this question.
Upvotes: 1
Views: 208
Reputation: 55453
You're consistently making a rather common error: taking a pointer to a Go string or slice. The problem with this is that both slices and strings
are in fact struct
s—of three and two fields, correspondingly. Hence when you're taking an address of a variable containing a slice or a string, that's an address of the first field of that struct, not of the first item contained in the container.
So, the first fix is to replace all places making use of
var v []whatever
p := unsafe.Pointer(&v)
and
var s string
p := unsafe.Pointer(&s)
with
var v []whatever
p := unsafe.Pointer(&v[0])
and
var s string
p := unsafe.Pointer(&s[0])
correspondingly.
Two more points to consider:
It's hard for me to figure exact semantics of VBByRefStr
just by reading a poorly-worded MSDN doc, but from cursory reading of some posts about it I have managed to google, it appears your target function has these parameters typed as PChar
.
If I'm correct in my assessment, the strings you pass to the function must be zero-terminated as they are not Delphi-native strings, and have no length field encoded in them.
Hence when preparing the strings I'd make sure you add the \x00
character at the end.
Note that in your example the Go strings are plain ASCII (and note that UTF-8 is ASCII-clean, i.e. UTF-8-encoded strings which contain only 7-bit ASCII characters to not need any re-encoding before passing them to anything which intends to see an ASCII data), and Windows code pages are ASCII-compatible as well. Hence for your case for a start I'd not mess with proper character set conversion and would do something like
func toPChar(s string) unsafe.Pointer {
dst := make([]byte, len(s)+1)
copy(dst, s)
dst[len(s)] = 0 // NUL terminator
return unsafe.Pointer(&dst[0])
}
…and once you make it work with plain ASCII strings, add character conversion in the mix.
Do not store the result of type-conversion of an unsafe.Pointer
to uintptr
in a variable!
This is a non-obvious thing, I admit, but the logic is as follows:
unsafe.Pointer
constitutes a live reference to the memory it points to (if the value is non-nil
, of course).uintptr
type is specifically defined as representing an opaque pointer-sized integer value, not a pointer. Hence an address stored in a variable of type uintptr
does not count as a live reference to the memory at that address.These rules mean when you take an address of some variable and store it in a variable of type uintptr
, it means you may have zero references
to that memory block. For instance, in this example
func getPtr() unsafe.Pointer
...
v := uintptr(getPtr())
the GC is free to reclaim the memory a pointer to which was returned by getPtr()
.
As a special case, Go guarantees that it's OK to use casting of unsafe.Pointer
to uintptr
in expressions which are evaluated to produce actual arguments to call a function. So, store intermediate pointers not as uintptr
s but rather than unsafe.Pointer
s and cast them at the function call site, like in
a1 := unsafe.Pointer(&whatever[0])
...
ret, _, _ := proc.Call(uintptr(a1), uintptr(a2),
uintptr(a3), uintptr(a4))
It would also be cool to see the actual Delphi-native type of the function you're calling.
Another problem might be mismatch in the calling convention but if I understand correctly, P/Invoke defaults to winapi
CC (which equals stdcall
on Windows), and since your P/Invoke wrapper does not list any CC explicitly, we may hope it's indeed winapi
so we should be fine here.
Upvotes: 1
Reputation: 987
Okay, so I've finally found an answer. As @kostix noticed go-ole
was required, since DLL uses COM and I had to call CoInitializeEx first.
But that was only part of this, turns out string parameters of procedure were declared as UnicodeString, which required me to put together a conversion function to properly fit into layout described in documentation: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Unicode_in_RAD_Studio#New_String_Type:_UnicodeString
And once that was done, all started to work as intended.
Upvotes: 1