Необработанное исключение delphi в сторонней dll

У меня есть динамическая библиотека, из которой мне нужно вызвать процедуру в моей программе 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

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

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

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

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

stackoverflow.com/questions/43385670/… Кроме того, как в коде go вы обрабатываете тот факт, что функция имеет тип возврата void? А как узнать, что два двойных массива достаточно длинные?
David Heffernan 10.09.2018 10:11

@DavidHeffernan Строки в go закодированы в utf-8, поэтому я вручную конвертирую их в Windows-1251 и передаю указатель на массив байтов в качестве параметра процедуры. Я не уверен, что нужно обрабатывать с возвратом типа void. У меня есть документация для этой dll, поэтому я знаю, какой размер массивов мне нужен, но в ней не указан формат строк.

K. Kowalczyk 10.09.2018 10:37

Pinvoke C# определяет ABI для вызова функции.

David Heffernan 10.09.2018 10:55
1
3
141
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вы постоянно делаете довольно частую ошибку: берете указатель на строку или фрагмент 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, представляет собой действующую ссылку на память, на которую она указывает (если, конечно, значение не относится к nil).
    • Тип uintptr конкретно определен как представляющий значение непрозрачное целое число размером с указатель, а не указатель. Следовательно, адрес, хранящийся в переменной типа uintptr, не считается действующей ссылкой на память по этому адресу.

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

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

GC может освободить память, указатель на которую был возвращен getPtr().

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

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

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


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

Спасибо за такое подробное объяснение, я многому из него научился. К сожалению, похоже, что проблема с соглашением о вызовах. Я отредактировал свой вопрос более подробно.

K. Kowalczyk 12.09.2018 15:10

Вы случайно можете (временно) использовать cgo, чтобы попытаться вызвать вашу функцию? С cgo вы должны иметь возможность указать соглашение о вызовах в объявлении функции, чтобы компилятор мог использовать правильное соглашение о вызовах. Другой возможный вектор для решения этой проблемы - попытаться связать эту DLL с собственной тестовой программой Delphi, которая будет указывать другое соглашение о вызовах для этой импортированной функции перед каждым циклом компиляция + выполнение, чтобы вы могли выяснить, какой из них правильный.

kostix 12.09.2018 16:25

Кстати, я был бы признателен, если бы вы держали меня в курсе вашего прогресса по этому вопросу (меня это интересует). ;-)

kostix 12.09.2018 16:26

Я на самом деле попробовал, но столкнулся с проблемами с компоновщиком, почему-то моя инструментальная цепочка не очень хорошо работает с 32-битной версией (большинство библиотек, которые мне нужны для этого проекта, не имеют 64-битных версий). Я попытаюсь очистить свою настройку позже, чтобы сделать это работает. Я сравниваю выполнение этой процедуры бок о бок, вызванной из рабочей программы и моей. Кажется, что он выполняется одинаково до вызова CoCreateInstance в библиотеке combase, но мне все равно нужно подтвердить это завтра.

K. Kowalczyk 12.09.2018 20:50

Хм, может быть, go-ole может с этим пригодиться?

kostix 12.09.2018 20:55

Я наконец выяснил, чем все это вызвано, и отправил ответ, go-ole действительно пригодился. Большое спасибо. :)

K. Kowalczyk 24.09.2018 15:53

Спасибо, что держали меня в курсе. Рад, что ты разобрался!

kostix 24.09.2018 18:09

(Прошу прощения за мою сломанную правку, не знаю, что там произошло!)

halfer 29.05.2020 20:41
Ответ принят как подходящий

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

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

И как только это было сделано, все заработало, как задумано.

Другие вопросы по теме