Import biblioteki DLL napisanej w C# do Delphi XE2

0
kAzek napisał(a):

Tak chyba nie zwrócisz. Raczej powinieneś spróbować przekazać tablicę jako parametr zwrotny to jakoś powinno się udać, możliwe że będzie też trzeba w kolejnym parametrze podać wielkość przekazanej tablicy.

Rzecz w tym, że taki standard narzuca aplikacja, do której mam napisać plugin. Nie mam w tym zakresie swobody ruchów. Docelowo Metoda ma przyjąć jeden parametr PChar, a oddać (no właśnie - wskaźnik na) tablicę packed record.

Udało mi się zrobić mały krok naprzód i wdaje mi się, że to jedyny logiczny kierunek - przekazanie wskaźnika:

        [ComVisible(true)]
        [DllExport("Function_3", CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
        public static IntPtr Function_3([MarshalAs(UnmanagedType.LPWStr)]string pParams)
        {
            MessageBox.Show(pParams);

            int[] arr0 = new int[] { 1, 2, 3 };
            MessageBox.Show("1");

            IntPtr ip0 = Marshal.AllocHGlobal(3 * Marshal.SizeOf(typeof(int)));
            MessageBox.Show("2");

            MessageBox.Show(ip0.ToString());

            return ip0;
        }
procedure TForm1.Button6Click(Sender: TObject);
var
  DLL : THandle;
  Function3 : function(pParams: PChar): Pointer; stdcall;
  p : Pointer;
begin
    DLL := LoadLibrary('d:\tmp1\mMPlugin_test1.dll');
    try
        @Function3 := GetProcAddress(DLL, 'Function_3');
        if @Function3=nil then raise Exception.Create('Bład - nie mogę znaleźć proceudry w bibliotece!');
        p:=Function3(PChar('test'));
    finally
        FreeLibrary(DLL);
    end;
end;

W takim wariancie metoda z C# jest prawidłowo wywoływana i przechodzi przez kolejne punkty kontrole (MessageBoxy), a ostatecznie tekst, który pojawia się w ostatnim z dziesiętną reprezentacją wskaźnika ip0 odpowiada wartości jaką otrzymuje zmienna p po stronie Delphi.

Natomiast - metoda działa dla int, ale jeżeli chcę zmienić na string (przy wyznaczaniu rozmiaru typu za pomocą funkcji Marshal.SizeOf) - z powrotem dostaję błąd E0434352 - podejrzewam, że chodzi o to, że string w C# jest typem zarządzanym, a Marshaling operuje na typach niezarządzanych i stąd cała heca.

Spróbuję dzisiaj przeskoczyć jeden krok i przekazać wskaźnik do struct, który będzie opakowany znacznikami Marshalingu i wtedy zobaczymy jak zareaguje Marshal.SizeOf

0

Osiągnąłem limitowany sukces - tj. jestem w stanie przesłać strukturę, ale mam problem z przesłaniem tablicy zawierającej strukturę.

Struktury mam zadeklarowane tak:

[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)]
    public struct PPP
    {
        [MarshalAs(UnmanagedType.I4)]
        public Int32 Field1;

        [MarshalAs(UnmanagedType.LPWStr)]
        public string Field2;

        [MarshalAs(UnmanagedType.LPWStr)]
        public string Field3;

        [MarshalAs(UnmanagedType.LPWStr)]
        public string Field4;

        [MarshalAs(UnmanagedType.I4)]
        public Int32 Field5; 
    }
TPD = packed record
    Field1: integer;
    Field2: PChar;
    Field3: PChar;
    Field4: PChar;
    Field5: integer;
  end;
  TPP = array of TPD;
  PPP = ^TPP;

I teraz - dla jednego egzemplarza struktury to działa:

[ComVisible(true)]
        [DllExport("Function_5", CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
        public static IntPtr Function_5([MarshalAs(UnmanagedType.LPWStr)]string pParams)
        {
            MessageBox.Show(pParams);

            PPP pp_array = new PPP();
            pp_array.Field1 = 2;
            pp_array.Field2 = "abc";
            pp_array.Field3 = "kkk";
            pp_array.Field4 = "fff";
            pp_array.Field5 = 1;

            MessageBox.Show("1");

            IntPtr ip0 = Marshal.AllocHGlobal(1 * Marshal.SizeOf(typeof(PPP)));
            MessageBox.Show("2");

            Marshal.StructureToPtr(pp_array, ip0, false); 
            MessageBox.Show("3");

            MessageBox.Show(ip0.ToString("X"));

            return ip0;
        }
procedure TForm1.Button8Click(Sender: TObject);
var
  DLL : THandle;
  Function5 : function(pParams: PChar): Pointer; stdcall;
  p : Pointer;
  TPP :  TPD;
begin
    DLL := LoadLibrary('biblioteka.dll');
    try
        @Function5 := GetProcAddress(DLL, 'Function_5');
        if @Function5=nil then raise Exception.Create('Bład - nie mogę znaleźć procedury w bibliotece!');
        p:=Function5(PChar('test'));
        TPP:= TPD(p^);
    finally
        FreeLibrary(DLL);
    end;
end;

Po wykonaniu rzutowania - zmienna TPP przechowuje dane przekazane z DLL. Owszem rzutuję uniwersalny wskaźnik na packed record

Natomiast jak chcę przekazać tablicę elementów typu struct, to po stronie C# cała metoda przechodzi mi bez błędu:

        [ComVisible(true)]
        [DllExport("Function_7", CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
        public static IntPtr Function_7([MarshalAs(UnmanagedType.LPWStr)]string pParams)
        {
            MessageBox.Show(pParams);

            int NOP = 2;

            PPP[] pp_array = new PPP[NOP];
            pp_array[0].Field1 = 2;
            pp_array[0].Field2 = "abc1";
            pp_array[0].Field3 = "kkk1";
            pp_array[0].Field4 = "fff1";
            pp_array[0].Field5 = 1;
            //------------
            pp_array[1].Field1 = 2;
            pp_array[1].Field2 = "abc2";
            pp_array[1].Field3 = "kkk2";
            pp_array[1].Field4 = "fff2";
            pp_array[1].Field5 = 2;

            MessageBox.Show("1");

            IntPtr ip0 = Marshal.AllocHGlobal(NOP * Marshal.SizeOf(typeof(PPP)));
            MessageBox.Show("2");

            for (int i = 0; i < NOP; i++)
            {
                Marshal.StructureToPtr(pp_array[i], ip0 + i * Marshal.SizeOf(typeof(PPP)), false);
                MessageBox.Show("3-"+i.ToString());
            }

            MessageBox.Show(ip0.ToString("X"));

            return ip0;
        }

Natomiast przy próbie rzutowania wskaźnika na - Delphi wywala mi błędy zapisu pod adresem fxxxxxx:

procedure TForm1.Button10Click(Sender: TObject);
var
  DLL : THandle;
  Function7 : function(pParams: PChar): PPP; stdcall;
  p : PPP;
  TPP1 :  TPP;
begin
    DLL := LoadLibrary('biblioteka.dll');
    try
        @Function7 := GetProcAddress(DLL, 'Function_7');
        if @Function7=nil then raise Exception.Create('Bład - nie mogę znaleźć procedury w bibliotece!');
        p:=Function7(PChar('test'));
        TPP1:= TPP(p^);
    finally
        FreeLibrary(DLL);
    end;
end;

Po stronie C# kod rozumiem tak, że najpierw metodą Marshal.AllocHGlobal rezerwuję/alokuję pamięć o takim rozmiarze jak mi potrzeba do zapełnienia n-razy rozmiar struktury, a potem za pomocą Marshal.StructureToPtr umieszczam w tym obszarze pamięci dane, w pętli przesuwając tylko offset względem początku danych reprezentowanych przez wskaźnik ip0.

Delphi tego nie rozumie, albo robię coś źle.

1

Delphi chce znać wielkość zwracanej tablicy co nie dziwne przecież alokowałeś pamięc tylko na 2 rekordy a tak próbuje nie wiadomo ile pamięci czytać więc nie dziwne, że się wywali.
czyli nie:

  TPP = array of TPD;

tylko:

  TPP = array [0..1] of TPD;

a później

var
  //...
  MyTPD: TPD;
begin
   //...
       TPP1:= TPP(p^);
        for MyTPD in TPP1 do
          ShowMessage(Format('Field1: %0:d Field2: %1:s Field3: %2:s Field4: %3:s Field5: %4:d',
            [MyTPD.Field1, MyTPD.Field2, MyTPD.Field3, MyTPD.Field4, MyTPD.Field5]));
  //..
end;

ewentualnie może zostać:

  TPP = array of TPD;

ale wtedy:

var
  //..
  aTPD: array of TPD;
  MyTPD: TPD;
begin
    //..
        p:=Function7(PChar('test'));
        SetLength(aTPD, 2); //tu musisz wiedzieć ile masz odczytać rekordów
        MoveMemory(@aTPD, p, 2* SizeOf(TPD)); //i tu też bo inaczej  wyjedziesz poza zakres zaalokowanej pamięci
        for MyTPD in aTPD do
          ShowMessage(Format('Field1: %0:d Field2: %1:s Field3: %2:s Field4: %3:s Field5: %4:d',
            [MyTPD.Field1, MyTPD.Field2, MyTPD.Field3, MyTPD.Field4, MyTPD.Field5]));
    //...
end;
0

Równie dobrze ta funkcja mogłaby zwracać w rezultacie pointer na blok danych macierzy, a rozmiar tablicy w parametrze zwrotnym. Chyba że docelowa macierz zawsze będzie miała taki sam rozmiar.

3

Probówałem coś wykombinować z tym UnmanagedExport ale mi ta biblioteka nie działa. Tu więc nie pomogę.
Ale postanowiłem wykorzystać C++/CLI jako platformę pośredniczącą.

Załóżmy że mamy taki kod w C# kompilowany do DLL-ki i to co chcemy odpalić z Delphi i odczytać z tego wyniki to funkcja Class1.Test.

// Class1.cs
using System;
using System.Collections.Generic;

public struct Strukt
{
    public int field1;
    public string field2, field3, field4;
    public int field5;
    public Strukt(int f1, string f2, string f3, string f4, int f5)
    {

        field1 = f1;
        field2 = f2;
        field3 = f3;
        field4 = f4;
        field5 = f5;
    }
}

public class Class1
{
    public static Strukt[] Test(string s)
    {
        return new Strukt[] {
            new Strukt(1, s, s+s, s+s+s, 100),
            new Strukt(2, s, s+s, s+s+s, 200),
            new Strukt(3, s, s+s, s+s+s, 300)
        };
    }
}

No to jedziemy: druga DLL-ka, jako pomost:

// biblioteka2_interop.h
#pragma once

#ifdef BIBLIOTEKA2INTEROP_EXPORTS
#define BIBLIOTEKA2INTEROP_API extern "C" __declspec(dllexport)
#else
#define BIBLIOTEKA2INTEROP_API extern "C" __declspec(dllimport)
#endif

struct NativeStrukt;
struct NativeTestHandle;

BIBLIOTEKA2INTEROP_API
NativeTestHandle* __stdcall Test(const wchar_t *s, NativeStrukt **results, int *numResults);

BIBLIOTEKA2INTEROP_API
void __stdcall TestFree(NativeTestHandle *nth);
// biblioteka2_interop.cpp
#include "biblioteka2_interop.h"
#include <msclr/marshal.h>

using namespace System;
using namespace msclr::interop;

struct NativeStrukt
{
	int field1;
	const wchar_t *field2, *field3, *field4;
	int field5;
};

struct NativeTestHandle
{
	gcroot<marshal_context^> ctx;
	NativeStrukt *results;
	NativeTestHandle(int numResults)
	{
		results = (NativeStrukt*)malloc(sizeof(NativeStrukt) * numResults);
		ctx = gcnew marshal_context();
	}
	~NativeTestHandle()
	{
		free(results);
	}
};

BIBLIOTEKA2INTEROP_API
NativeTestHandle* __stdcall Test(const wchar_t *s, NativeStrukt **results, int *numResults)
{
	auto lista = Class1::Test(gcnew String(s));
	int num = lista->Length;
	
	auto nth = new NativeTestHandle(num);
	
	for (int i = 0; i < num; i++)
	{
		nth->results[i].field1 = lista[i].field1;
		nth->results[i].field2 = nth->ctx->marshal_as<const wchar_t*>(lista[i].field2);
		nth->results[i].field3 = nth->ctx->marshal_as<const wchar_t*>(lista[i].field3);
		nth->results[i].field4 = nth->ctx->marshal_as<const wchar_t*>(lista[i].field4);
		nth->results[i].field5 = lista[i].field5;
	}
	
	*numResults = num;
	
	*results = nth->results;
	return nth;
}

BIBLIOTEKA2INTEROP_API
void __stdcall TestFree(NativeTestHandle *nth)
{
	delete nth;
}

Nie gwarantuję 100% poprawności i że nie ma wycieków... w każdym razie wydaje się działać:

// Unit1.pas

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Label1: TLabel;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}
{$POINTERMATH ON}

type
  PNativeStrukt = ^TNativeStrukt;
  TNativeStrukt = record
    field1 : integer;
    field2, field3, field4 : pchar;
    field5 : integer;
  end;

  TTest = function(a : PChar; out results : PNativeStrukt; out numResults : integer) : pointer; stdcall;
  TTestFree = procedure(handle:pointer); stdcall;

procedure TForm1.Button1Click(Sender: TObject);
var
  DLL : THandle;
  Test : TTest;
  TestFree : TTestFree;
  results : PNativeStrukt;
  numResults : integer;
  testHandle : pointer;
  i:integer;
  s:string;
begin
  DLL := LoadLibrary('biblioteka2interop.dll');
  @Test := GetProcAddress(DLL, '_Test@12');
  @TestFree := GetProcAddress(DLL, '_TestFree@4');

  testHandle := Test('Ala', results, numResults);
  Label1.Caption := inttostr(numResults);

  for i:=0 to numResults-1 do
  begin
    s:=Format('%d %s %s %s %d', [results[i].field1, string(results[i].field2), string(results[i].field3), string(results[i].field4), results[i].field5]);
    Memo1.Lines.Add(s);
  end;

  TestFree(testHandle);

  FreeLibrary(DLL);
end;

end.

hhh.png

Użyłem Visual Studio 2017 i Delphi 10.1 Berlin.

0
Azarien napisał(a):

Probówałem coś wykombinować z tym UnmanagedExport ale mi ta biblioteka nie działa. Tu więc nie pomogę.

Instalowałeś przez NuGeta, czy ręcznie ?

0

Witam, instaluję wg instrukcji kAzek i wywala błąd:

Package c:\program files (x86)\borland\delphi7\Projects\Bpl\IndySystem70.bpl can't be installed because it is not a design time package.

Może ktoś pomóc ?

1 użytkowników online, w tym zalogowanych: 0, gości: 1