Необработанное исключение Delphi в сторонних DLL - PullRequest
0 голосов
/ 10 сентября 2018

У меня есть динамическая библиотека, из которой мне нужно вызывать процедуру в моей программе Go, но я не могу понять, что это правильно. У меня есть другая программа, написанная на C #, которая использует эту DLL и в соответствии с dnSpy dllimport компилирует в:

[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);

Поэтому, основываясь на этом, я попытался назвать это в своем коде так:

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
}

Это приводит к необработанному исключению delphi:

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

Я полагаю, что проблема может заключаться в том, как я передаю эти строки в процедуру, но я не вижу, чем мой код отличается от этого приложения на C #.

В библиотеке есть несколько других процедур, я могу без проблем вызвать DLL_Version:

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)

Выходы:

2018/09/10 09:13:11 DLL version: 1.250000

EDIT: Я считаю, что я выяснил, что вызывает исключение. Кажется, что при вызове из моего кода 2-й и 3-й параметры для этой процедуры, которые должны быть указателями на строковые буферы, вместо этого являются указателями на указатели на эти буферы, если я нарушу начало процедуры и исправлю это вручную, код запустится и завершается правильно. Я до сих пор не нашел причину, почему это происходит, и как это исправить.

EDIT2: По-видимому, моя предыдущая оценка этого дела была неверной. Мне удалось остановить выдачу исключения программой, но я проанализировал сборку и оказалось, что это произошло только из-за выхода программы до того, как было сгенерировано это исключение, и вместо исключения я получил нормальный код ошибки о недопустимой строке.

Хотя я не очень хорошо разбираюсь в дизассемблированном коде, похоже, что проблема действительно может быть вызвана несоответствием соглашения о вызовах. В начале кода процедуры при вызове из рабочей программы регистры EAX, EBX, ECX и EDX пусты, а при вызове из моего кода только EAX и ECX пусты, EDX содержит что-то, но EBX указывает на указатель процедуры Calc в моем исполняемом файле. , Позже в DLL есть часть, которая проверяет, является ли EBX пустым, и выдает исключение, если нет.

РЕДАКТИРОВАТЬ 3: Ну, это, кажется, также не связано, я знаю, что слишком мало информации, чтобы кто-либо мог догадаться о том, что происходит. Поэтому я постараюсь отследить, откуда именно возникло это исключение, и вернусь к этому вопросу.

Ответы [ 2 ]

0 голосов
/ 24 сентября 2018

Хорошо, так что я наконец-то нашел ответ. Как заметил @kostix, go-ole требовалось, так как DLL использует COM, и мне пришлось сначала вызвать CoInitializeEx.

Но это было только частью этого, оказалось, что строковые параметры процедуры были объявлены как UnicodeString, что потребовало от меня собрать функцию преобразования для правильного размещения в макете, описанном в документации: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Unicode_in_RAD_Studio#New_String_Type:_UnicodeString

И как только это было сделано, все начали работать как положено.

0 голосов
/ 10 сентября 2018

Вы постоянно совершаете довольно распространенную ошибку: берете указатель на строку или фрагмент Go. Проблема в том, что как ломтики, так и строки на самом деле struct с - из три и два поля соответственно. Следовательно, когда вы берете адрес переменной, содержащей фрагмент или строка, это адрес первого поля этой структуры, не первого элемента, содержащегося в контейнере.

Итак, первое исправление - заменить все места используя

var v []whatever
p := unsafe.Pointer(&v)

и

var s string
p := unsafe.Pointer(&s)

с

var v []whatever
p := unsafe.Pointer(&v[0])

и

var s string
p := unsafe.Pointer(&s[0])

соответственно.

Еще два момента для рассмотрения:

  • Мне трудно определить точную семантику VBByRefStr просто прочитав плохо сформулированный документ MSDN, но из краткого читая некоторые посты об этом мне удалось в гугле, кажется, ваша целевая функция имеет эти параметры как PChar.

    Если я прав в моей оценке, строки, которые вы передаете к функции должны заканчиваться нулем, так как они не Delphi-нативные строки, и не имеют поля длины, закодированного в их.

    Поэтому при подготовке струн я обязательно добавлю символ \x00 в конце.

    Обратите внимание, что в вашем примере строки Go являются простыми ASCII (и обратите внимание, что UTF-8 является ASCII-чистым, т.е. кодируется в UTF-8 строки, которые содержат только 7-битные символы ASCII, чтобы не нуждаться любое перекодирование перед передачей их чему-либо, что намеревается увидеть данные ASCII), и кодовые страницы Windows также совместимы с ASCII. Поэтому для вашего случая для начала я бы не связывался с правильным преобразованием набора символов и делал бы что-то вроде

    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])
    }
    

    … и как только вы сделаете это с простыми ASCII-строками, добавить преобразование символов в микс.

  • Не хранить результат преобразования типов от unsafe.Pointer до uintptr в переменной!

    Признаюсь, это неочевидная вещь, но логика такова:

    • Для Go GC - переменная, содержащая значение типа unsafe.Pointer представляет собой живую ссылку на память он указывает на (если значение не-1050 *, конечно).
    • Тип uintptr специально определен как представляющий непрозрачное целое число размером с указатель , а не указатель. Следовательно, адрес хранится в переменной типа uintptr не считается живой ссылкой на память по этому адресу.

    Эти правила означают, когда вы берете адрес некоторая переменная и сохранить ее в переменной типа uintptr, это означает, что у вас может быть ноль ссылок в этот блок памяти. Например, в этом примере

    func getPtr() unsafe.Pointer
    ...
    v := uintptr(getPtr())
    

    ГК может свободно вернуть в память указатель на который был возвращен getPtr().

    В качестве особого случая Go гарантирует, что его можно использовать. приведение unsafe.Pointer к uintptr в выражениях которые оцениваются для создания фактических аргументов для вызова функции. Таким образом, храните промежуточные указатели не как uintptr s, а скорее чем unsafe.Pointer s и приведите их на сайт вызова функции, как в

    a1 := unsafe.Pointer(&whatever[0])
    ...
    ret, _, _ := proc.Call(uintptr(a1), uintptr(a2),
        uintptr(a3), uintptr(a4))
    

Надеюсь, это поможет.

Было бы круто увидеть реальный тип Delphi-native функции, которую вы вызываете.


Другая проблема может быть несовпадением в соглашении о вызовах но по умолчанию IIUC P / Invoke winapi CC (что равно stdcall в Windows), и так как ваш P / Invoke Оболочка не перечисляет никаких CC явно, мы можем надеяться, что это на самом деле winapi так что у нас все будет хорошо.

...