How to display the values from structures in C# fr

2020-07-09 04:31发布

问题:

abc.h file

typedef struct sp_BankNoteTypeList
{
    int cim_usNumOfNoteTypes;
    struct sp_notetype
    {
            USHORT cim_usNoteID;
            CHAR   cim_cCurrencyID[3];
            ULONG  cim_ulValues;
            bool   cim_bConfigured;
    }SP_CIMNOTETYPE[12];
}SP_CIMNOTETYPELIST,*SP_LPCIMNOTETYPELIST;


BNA_API int BanknoteType(SP_CIMNOTETYPELIST *sp_BankNoteType);

abc.cpp (DLL File)

int BanknoteType(SP_CIMNOTETYPELIST *sp_BankNoteType)
{
    LPWFSCIMNOTETYPE    fw_notetypedata;
    LPWFSCIMNOTETYPELIST    lpNoteTypeList;   //output param
    hResult = WFSGetInfo(hService, WFS_INF_CIM_BANKNOTE_TYPES, (LPVOID)NULL, 400000, &res);
    lpNoteTypeList=(LPWFSCIMNOTETYPELIST)res->lpBuffer;
    if(hResult!=0)
    {
            return (int)hResult;
    }
    sp_BankNoteType->cim_usNumOfNoteTypes = lpNoteTypeList->usNumOfNoteTypes;
    for(int i=0;i<lpNoteTypeList->usNumOfNoteTypes;i++)
    {
        sp_BankNoteType->SP_CIMNOTETYPE[i].cim_usNoteID = lpNoteTypeList->lppNoteTypes[i]->usNoteID;
        sp_BankNoteType->SP_CIMNOTETYPE[i].cim_ulValues = lpNoteTypeList->lppNoteTypes[i]->ulValues;
        sp_BankNoteType->SP_CIMNOTETYPE[i].cim_bConfigured = lpNoteTypeList->lppNoteTypes[i]->bConfigured;
        sp_BankNoteType->SP_CIMNOTETYPE[i].cim_cCurrencyID[0] = lpNoteTypeList->lppNoteTypes[i]->cCurrencyID[0];
        sp_BankNoteType->SP_CIMNOTETYPE[i].cim_cCurrencyID[1] = lpNoteTypeList->lppNoteTypes[i]->cCurrencyID[1];
        sp_BankNoteType->SP_CIMNOTETYPE[i].cim_cCurrencyID[2] = lpNoteTypeList->lppNoteTypes[i]->cCurrencyID[2];        

    } 
    return (int)hResult;
}

Structure :-

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct sp_notetype
        {
            public ushort cim_usNoteID;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
            public char[] cim_cCurrencyID;
            public ulong cim_ulValues;
            public bool cim_bConfigured;
        };

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct sp_BankNoteTypeList
        { 
            public int cim_usNumOfNoteTypes;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
            public sp_notetype[] SP_CIMNOTETYPE;    
        }; 
        public sp_notetype[] SP_CIMNOTETYPE;
        public sp_BankNoteTypeList SP_CIMNOTETYPELIST;

Function calling:-

[DllImport(@"abc.dll")]
        public static extern int BanknoteType(out sp_BankNoteTypeList SP_CIMNOTETYPELIST);



     public string BNA_BankNoteType(out int[] NoteID,out string[]CurrencyID,out string[] Values,out bool[] Configured)
    {
        NoteID = new int[12];
        CurrencyID = new string[12];
        Values = new string[12];
        Configured = new bool[12];
        try
        {
             trace.WriteToTrace(" Entered in BNA_BankNoteType ", 1);
             hResult = BanknoteType(out SP_CIMNOTETYPELIST);
            for (int i = 0; i < 12; i++)
            {
                NoteID[i] = (SP_CIMNOTETYPELIST.SP_CIMNOTETYPE[i].cim_usNoteID);
                CurrencyID[i] = (SP_CIMNOTETYPELIST.SP_CIMNOTETYPE[i].cim_cCurrencyID[0]).ToString() + (SP_CIMNOTETYPELIST.SP_CIMNOTETYPE[i].cim_cCurrencyID[1]).ToString() + (SP_CIMNOTETYPELIST.SP_CIMNOTETYPE[i].cim_cCurrencyID[2]).ToString();
                Values[i] = (SP_CIMNOTETYPELIST.SP_CIMNOTETYPE[i].cim_ulValues).ToString();
                Configured[i] = (SP_CIMNOTETYPELIST.SP_CIMNOTETYPE[i].cim_bConfigured);

            }

            return DicErrorCode(hResult.ToString());
        }
        catch (Exception ex)
        {

            return "FATAL_ERROR";
        }

when I try calling them in C# I get garbage value in C#. While I debug them I find proper storing the values. Any help as to how the values must be passed would be very useful.. Thanks in advance.

回答1:

The C# declarations are on the right track, it is just that the details are subtly wrong. A good starting point is this post, shows you how to write some test code to verify that the structure declarations are a good match. Doing so on this particular one:

C++: auto len = sizeof(SP_CIMNOTETYPELIST);                    // 196 bytes
C# : var len = Marshal.SizeOf(typeof(sp_BankNoteTypeList));    // 296 bytes

Not close, you must get an exact match to have any hope of it marshalling correctly. The C# declaration for the inner struct should look like this:

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct sp_notetype {
        public ushort cim_usNoteID;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
        public char[] cim_cCurrencyID;
        public uint cim_ulValues;
        private byte _cim_bConfigured;
        public bool cim_bConfigured {
            get { return _cim_bConfigured != 0; }
        }
    };

    [DllImport(@"abc.dll", CallingConvention = CallingConvention.Stdcall)]
    public static extern int BanknoteType([Out]out sp_BankNoteTypeList list);

The sp_BankNoteTypeList declaration is okay. Rerun the test and you should now get 196 bytes in C# as well. Annotating the changes:

  • CharSet = CharSet.Ansi
    That one was necessary to get the CHAR member to marshal correctly. It is a Windows typedef for char, an 8 bit type. CharSet.Auto would have been the right choice if the native structure used WCHAR.
  • public uint cim_ulValues;
    Windows uses the LLP64 data model, that keeps a ULONG at 32 bits in both 32-bit and 64-bit programs. That makes uint the correct C# equivalent instead of ulong.
  • private byte _cim_bConfigured;
    bool is a very tricky type with poor standardization. It is 1 byte in C++, 4 bytes in C, 2 bytes in COM interop, 1 byte in managed code. The default marshaling assumes BOOL as the matching native type, the way it is done in the winapi. Declaring it private as a byte with a public property getter is one way to do it and the one I favored here, applying the [MarshalAs(UnmanagedType.U1)] attribute to the field would be another.
  • CallingConvention = CallingConvention.Stdcall
    Pretty important to be explicit about this, the other common selection here is CallingConvention.Cdecl. I can't tell from the native snippet which one is correct, BNA_API is a macro, but you did not mention the PInvokeStackImbalance MDA complaining about it so Stdcall is somewhat likely to be correct. Do make sure you did not turn it off.
  • [Out]out sp_BankNoteTypeList list
    The [Out] attribute is necessary here to convince the pinvoke marshaller that copying the structure back is required. This is pretty unintuitive, most programmer think that out is enough. But that is a C# language detail that the marshaller does not know about. Copying back has to be explicitly requested, the structure is not "blittable". Or in other words, the native layout is not the same as the internal managed layout. The ByValArray makes that inevitable.

Quite a laundry-list, I hope I got them all. Getting the structure sizes the same is 95% of the battle.



回答2:

I've recreated the C++ part, since I got errors related to WFSGetInfo and its related structs, I'm populating fields with some random data (a note about char[3] field: I'm populating it with 3 random uppercase letters). With inspiration from MSDN and many other questions from SO, I've identified a list of problems in the code; after fixing those problems, I was able to get the right data out from C#. As a note, I'm using VStudio2015.

Couple of ground notes:

  • BNA_API for me is __declspec(dllexport)
  • The function declaration is placed inside a

    #if defined(__cplusplus)
    extern "C" {
    #endif
    
    // Function declaration comes here
    
    #if defined(__cplusplus)
    }
    #endif
    

    block, to avoid C++ name mangling. For more details, check [MSDN]: Decorated Names

Problems:

  1. Calling convention mismatch (this was throwing an exception in my case): by default C(C++) uses __cdecl while the C# marshaler uses __stdcall. That doesn't work well, it corrupts the stack when pushing/popping arguments. In order to fix this you must change the default in one place only:
    • C++: BNA_API int __stdcall BanknoteType(SP_CIMNOTETYPELIST *sp_BankNoteType); - this is the method I chose
    • C#: DllImport(@"abc.dll", CallingConvention = CallingConvention.Cdecl)] - this works as well
  2. Some of the types from C# are different(wider) than their correspondents from C. When data is aligned, if such mismatches occur, everything that comes after the 1st one is messed up (when trying to get it out). So, in struct sp_notetype's C# definition, you should have: public uint cim_ulValues;. Check [MSDN]: Marshaling Arguments for the complete list
  3. (could be a variant of the previous one) The Charset of your structures - in C, char (8bit wide) is used -, change it from Charset.Auto to Charset.Ansi. Check [SO]: C# calling C DLL, pass char * as parameter not correct