Appelez le code non géré à partir de C # - retour d'une structure avec des tableaux
-
22-07-2019 - |
Question
[EDIT] J'ai changé le source comme suggéré par Stephen Martin (en gras). Et ajouté le code source C ++ également.
J'aimerais appeler une fonction non gérée dans une dll auto-écrite en C ++. Cette bibliothèque lit la mémoire partagée de la machine pour obtenir des informations sur l'état d'un logiciel tiers. Puisqu'il y a deux valeurs, j'aimerais retourner les valeurs dans une structure. Cependant, dans la structure, il y a char []
(tableaux de caractères de taille fixe). J'essaie maintenant de recevoir cette structure de l'appel de la DLL comme ceci:
[StructLayout(LayoutKind.Sequential)]
public struct SYSTEM_OUTPUT
{
UInt16 ReadyForConnect;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
String VersionStr;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)]
String NameOfFile;
// actually more of those
}
public partial class Form1 : Form
{
public SYSTEM_OUTPUT output;
[DllImport("testeshm.dll", EntryPoint="getStatus")]
public extern static int getStatus(out SYSTEM_OUTPUT output);
public Form1()
{
InitializeComponent();
}
private void ReadSharedMem_Click(object sender, EventArgs e)
{
try
{
label1.Text = getStatus(out output).ToString();
}
catch (AccessViolationException ave)
{
label1.Text = ave.Message;
}
}
}
Je posterai aussi le code de la dll c ++, je suis sûr qu'il y a encore beaucoup à faire. La structure originale STATUS_DATA
contient un tableau de quatre instances de la structure SYSTEM_CHARACTERISTICS
et contient des char []
, qui ne sont pas rempli (encore), résultant en un mauvais pointeur. C'est pourquoi j'essaie d'extraire un sous-ensemble du premier élément SYSTEM_CHARACTERISTICS
dans STATUS_DATA
.
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <tchar.h>
#include <iostream>
#if defined(_MSC_VER)
#include <windows.h>
#define DLL extern "C" __declspec(dllexport)
#else
#define DLL
#endif
using namespace std;
enum { SYSID_LEN = 1024, VERS_LEN = 128, SCENE_LEN = 1024 };
enum { MAX_ENGINES = 4 };
struct SYSTEM_CHARACTERISTICS
{
unsigned short ReadyForConnect;
char VizVersionStr[VERS_LEN];
char NameOfFile[SCENE_LEN];
char Unimplemented[SCENE_LEN]; // not implemented yet, resulting to bad pointer, which I want to exclude (reason to have SYSTEM_OUTPUT)
};
struct SYSTEM_OUTPUT
{
unsigned short ReadyForConnect;
char VizVersionStr[VERS_LEN];
char NameOfFile[SCENE_LEN];
};
struct STATUS_DATA
{
SYSTEM_CHARACTERISTICS engine[MAX_ENGINES];
};
TCHAR szName[]=TEXT("E_STATUS");
DLL int getStatus(SYSTEM_OUTPUT* output)
{
HANDLE hMapFile;
STATUS_DATA* pBuf;
hMapFile = OpenFileMapping(
FILE_MAP_READ, // read access
FALSE, // do not inherit the name
szName); // name of mapping object
if (hMapFile == NULL)
{
_tprintf(TEXT("Could not open file mapping object (%d).\n"),
GetLastError());
return -2;
}
pBuf = (STATUS_DATA*) MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, 0);
if (pBuf == NULL)
{
_tprintf(TEXT("Could not map view of file (%d).\n"),
GetLastError());
CloseHandle(hMapFile);
return -1;
}
output->ReadyForConnect = pBuf->engine[0].ReadyForConnect;
memcpy(output->VizVersionStr, pBuf->engine[0].VizVersionStr, sizeof(pBuf->engine[0].VizVersionStr));
memcpy(output->NameOfFile, pBuf->engine[0].NameOfFile, sizeof(pBuf->engine[0].NameOfFile));
CloseHandle(hMapFile);
UnmapViewOfFile(pBuf);
return 0;
}
Maintenant, je reçois une structure output
vide et la valeur de retour est différente de 0 comme prévu. C'est plutôt un nombre changeant avec sept chiffres, ce qui me laisse perplexe ... Ai-je foiré dans la dll? Si je fais le code non managé et le débogue, je peux voir que la sortie
est remplie avec les valeurs appropriées.
La solution
Lors du retour d'informations dans une structure, la méthode standard consiste à passer un pointeur sur une structure en tant que paramètre de la méthode. La méthode remplit les membres de la structure, puis renvoie un code de statut (ou booléen). Donc, vous voulez probablement changer votre méthode C ++ pour prendre un SYSTEM_OUTPUT * et renvoyer 0 en cas de succès ou un code d'erreur:
public partial class Form1 : Form
{
public SYSTEM_OUTPUT output;
[DllImport("testeshm.dll", EntryPoint="getStatus")]
public extern static int getStatus(out SYSTEM_OUTPUT output);
public Form1()
{
InitializeComponent();
}
private void ReadSharedMem_Click(object sender, EventArgs e)
{
try
{
if(getStatus(out output) != 0)
{
//Do something about error.
}
}
catch (AccessViolationException ave)
{
label1.Text = ave.Message;
}
}
}
Autres conseils
- Assurez-vous que votre champ ReadyForConnect n'est pas rempli jusqu'à 4 octets. Dans mon projet, tous les champs int courts (2 octets) étaient remplis d'octets factices à 4 octets dans une DLL non gérée. Si tel est le problème, vous devriez organiser la structure de cette façon:
[StructLayout(LayoutKind.Sequential)] public struct SYSTEM_OUTPUT { [MarshalAs(UnmanagedType.I2)] UInt16 ReadyForConnect; [MarshalAs(UnmanagedType.ByValArray, ArraySubType=UnmanagedType.I1, SizeConst=2)] byte[] aligment; // 2 byte aligment up to 4 bytes margin [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] String VersionStr; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)] String NameOfFile; // ... }
- Si les chaînes sont des chaînes à terminaison ANSI, vous pouvez les annoter comme suit:
[MarshalAs(UnmanagedType.LPStr)] public String VersionStr;
Vous ne marshalez aucune donnée sur le côté géré. Lorsque vous déclarez output
du côté géré, sa valeur par défaut est null
. Ensuite, côté non géré, vous n'allouez jamais de mémoire pour sortie
. Vous devez allouer de la mémoire non gérée, passer le pointeur sur cette mémoire à votre fonction dll, puis marshaler le pointeur de cette mémoire sur votre struct:
[StructLayout(LayoutKind.Sequential, Pack = 4, CharSet = CharSet.Ansi)]
public struct SYSTEM_OUTPUT
{
UInt16 ReadyForConnect;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
String VersionStr;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)]
String NameOfFile;
// actually more of those
}
public partial class Form1 : Form
{
public SYSTEM_OUTPUT output;
[DllImport("testeshm.dll", EntryPoint="getStatus")]
public extern static int getStatus(IntPtr output);
public Form1()
{
InitializeComponent();
}
private void ReadSharedMem_Click(object sender, EventArgs e)
{
IntPtr ptr;
try
{
ptr = Marshall.AllocHGlobal(Marshall.SizeOf(typeof(SYSTEM_OUTPUT)));
int ret = getStatus(ptr);
if(ret == 0)
{
output = (SYSTEM_OUTPUT)Marshal.PtrToStructure(ptr, typeof(SYSTEM_OUTPUT));
}
//do something with output
label1.Text = ret;
}
catch (AccessViolationException ave)
{
label1.Text = ave.Message;
}
finally
{
Marshal.FreeHGlobal(ptr); //make sure to free the memory
}
}
}
Modifier:
Votre problème pourrait être un problème avec la différence entre emballage stratégies. J'ai mis à jour la définition de la structure.
EDIT: Je suis en train de réécrire toute cette réponse.
J'ai pris tout votre code C ++ et C #, je l'ai inséré dans une solution et je l'ai exécuté - et tout fonctionne pour moi. Je n'avais pas vos données de mappage de mémoire spécifiques, alors je l'ai simulé en remplissant pBuf avec de fausses données, et tout le reste bien. la valeur de retour et la structure de sortie sont correctes.
Quelque chose ne va pas avec les paramètres de votre projet? Cela semble idiot, mais vous avez mentionné l'exécution et le débogage du code non endommagé; vous construisez une dll bien?
Qu'est-ce que vous essayez de faire est possible, mais je pense que vous résolvez le mauvais problème.
Pourquoi ne pas lire le fichier mappé en mémoire directement à partir de C #? Consultez Winterdom.IO.FileMap
.Je l'ai utilisé et cela fonctionne bien.
MemoryMappedFile file = MemoryMappedFile.Open(FileMapRead, name);
using (Stream stream = memoryMappedFile.MapView(MapAccess.FileMapAllAccess, 0, length))
{
// here read the information that you need
}
Avec cela, vous n’êtes pas fini - vous devez toujours convertir un tampon d’octets en structure, mais vous êtes tous du côté géré et ce sera plus facile.
qui a alloué la mémoire pour la structure? Vous ne pouvez pas supprimer la mémoire native du segment de mémoire géré. En règle générale, la DLL native doit allouer sur le tas COM si elle s'attend à ce que l'appelant libère de la mémoire, ou renvoyer une interface de rappel telle qu'IMalloc pour libérer la mémoire renvoyée. Cela signifie que vous devez recevoir l'adresse de la mémoire de résultat sous la forme IntPtr et utiliser System.Runtime.InteropServices.Marshal pour copier les données d'un segment de mémoire natif vers un segment de mémoire géré (éventuellement dans une structure) avant de libérer de la mémoire.
Modifier pour la signature de fonction mise à jour: utilise public statique extern int getStatus (réf. sortie SYSTEM_OUTPUT); Vous n'allouez pas sur le tas COM dans la fonction native, la sortie est donc inutile.
Avez-vous envisagé d’ajouter un assemblage C ++ / CLI à votre projet? C'est un moyen extrêmement simple et puissant de combler le fossé entre le code géré et le code non géré. Je l'utilise beaucoup moi-même.