K. Kowalczyk
K. Kowalczyk

Reputation: 987

Unhandled delphi exception in 3rd party dll

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

Answers (2)

kostix
kostix

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 structs—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:

    • For Go GC, a variable containing a value of type unsafe.Pointerconstitutes a live reference to the memory it points to (if the value is non-nil, of course).
    • The 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 uintptrs but rather than unsafe.Pointers 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

K. Kowalczyk
K. Kowalczyk

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

Related Questions