tolsen64
tolsen64

Reputation: 999

How to marshal a structure in .NET to be used by native code?

I'm trying to write a DLL in .NET that can be called from a C++ executable. The executable expects a specific DLL to exist in its folder and expects a specific function name to be exported for it to consume. I'm using info from this Unmanaged Exports page to do it.

I have the following struct in C++ which I have to accept when the .NET function is called:

#pragma pack(4)

typedef struct sFMSelectorData
{
    // sizeof(sFMSelectorData)
    int nStructSize;

    // game version string as returned by AppName() (ie. in the form "Thief 2 Final 1.19")
    const char *sGameVersion;

    // supplied initial FM root path (the FM Selector may change this)
    char *sRootPath;
    int nMaxRootLen;

    // buffer to copy the selected FM name
    char *sName;
    int nMaxNameLen;

    // set to non-zero when selector is invoked after game exit (if requested during game start)
    int bExitedGame;
    // FM selector should set this to non-zero if it wants to be invoked after game exits (only done for FMs)
    int bRunAfterGame;

    // optional list of paths to exclude from mod_path/uber_mod_path in + separated format and like the config
    // vars, or if "*" all mod paths are excluded (leave buffer empty for no excludes)
    // the specified exclude paths work as if they had a "*\" wildcard prefix
    char *sModExcludePaths;
    int nMaxModExcludeLen;
} sFMSelectorData;

But I haven't the slightest clue how to marshal everything. Here's my structure currently. You can see I've been trying to experiment. If I remove all the marshalling attributes, I get the MessageBox when the C++ code calls the function (below), but the data in the variables of the structure are not what's expected. When I attempt to add marshalling attributes like this example, the C++ code crashes and terminates. I was trying to match the #pragma pack(4) from the C++ structure layout, but not sure how to fiddle with the strings to make them compatible with what I guess are pointers in the C++ struct. Also, I'm guessing that the <FieldOffset(0)> attributes refers to the byte index of that variable within the struct. I had to stop there and decided to post this question.

<StructLayout(LayoutKind.Sequential, Pack:=4)>
Public Structure sFMSelectorData
    ' sizeof(sFMSelectorData)
    Dim nStructSize As Integer

    ' game version string as returned by AppName() (ie. in the form "Thief 2 Final 1.19")
    <MarshalAs(UnmanagedType.LPStr)>
    Dim sGameVersion As String

    ' supplied initial FM root path (the FM Selector may change this)
    <MarshalAs(UnmanagedType.LPStr)>
    Dim sRootPath As String
    Dim nMaxRootLen As Integer

    ' buffer to copy the selected FM name
    <MarshalAs(UnmanagedType.LPStr)>
    Dim sName As String
    Dim nMaxNameLen As Integer

    ' set to non-zero when selector Is invoked after game exit (if requested during game start)
    Dim bExitedGame As Integer
    ' FM selector should set this to non-zero if it wants to be invoked after game exits (only done for FMs)
    Dim bRunAfterGame As Integer

    ' optional list of paths to exclude from mod_path/uber_mod_path in + separated format And Like the config
    ' vars, Or if "*" all Mod paths are excluded (leave buffer empty for no excludes)
    ' the specified exclude paths work as if they had a "*\" wildcard prefix
    <MarshalAs(UnmanagedType.LPStr)>
    Dim sModExcludePaths As String
    Dim nMaxModExcludeLen As Integer
End Structure

So the C++ code is calling this .NET function:

<DllExport(CallingConvention:=CallingConvention.Cdecl, ExportName:="SelectFM")>
Public Function SelectFM(<MarshalAs(UnmanagedType.Struct)> ByRef data As sFMSelectorData) As Int32

    Select Case MsgBox("Start the game?", MsgBoxStyle.Question Or MsgBoxStyle.YesNo, data.nStructSize)
        Case MsgBoxResult.Yes : Return eFMSelReturn.kSelFMRet_Cancel
        Case MsgBoxResult.No : Return eFMSelReturn.kSelFMRet_ExitGame
    End Select

    Return eFMSelReturn.kSelFMRet_Cancel

End Function

It does what I want. When I click Yes in the MessageBox, the game starts. When I click No, the game closes. But I need to use the data that's supposed to be populated into the structure. I'm not there yet.

Here's what the documentation says

An FM selector is a separate library (DLL) containing a utility, usually a UI based application, that lists the available FMs and lets the user pick which one to run. A selector could range from a simple list box with the FM names to a full blown manager with extended info, last played timestamps, sorting/filtering etc.

The default name for the selector is "FMSEL.DLL", but can be configured with the "fm_selector" cam_mod.ini var.

Exports

The DLL only needs to have a single symbol exported "SelectFM", which is a function in the form of:

int __cdecl SelectFM(sFMSelectorData *data);

The following return values are defined:

0 = 'data->sName' is expected to contain the selected FM name, if string is empty it means no FM 1 = cancel and exit game

Any other value is interpreted as cancel-and-continue, the game will start using the cam_mod.ini based active FM if defined, otherwise it will run without any FM.

Data types

#pragma pack(4)

typedef struct sFMSelectorData {  // sizeof(sFMSelectorData)  int structSize;

    // game version string as returned by AppName() (ie. in the form "Thief 2 Final 1.19")
    const char *sGameVersion;

    // supplied initial FM root path (the FM selector may change this)
    char *sRootPath;  int nMaxRootLen;

    // buffer to copy the selected FM name
    char *sName;  int nMaxNameLen;

    // set to non-zero when selector is invoked after game exit (if requested during game start)
    int bExitedGame;

    // FM selector should set this to non-zero if it wants to be invoked after game exits (only done for FMs)
    int bRunAfterGame;

    // optional list of paths to exclude from mod_path/uber_mod_path in + separated format and like the config
    // vars, or if "*" all mod paths are excluded (leave buffer empty for no excludes)
    // the specified exclude paths work as if they had a "*\" wildcard prefix
    char *sModExcludePaths;   int nMaxModExcludeLen;

    // language setting for FM (set by the FM selector when an FM is selected), may be empty if FM has no
    // language specific resources
    // when 'bForceLanguage' is 0 this is used to ensure an FM runs correctly even if it doesn't support
    // the game's current language setting (set by the "language" config var)
    // when 'bForceLanguage' is 1 this is used to force a language (that must be supported by the FM) other
    // than the game's current language
    char *sLanguage; int nLanguageLen; int bForceLanguage;
} sFMSelectorData;

#pragma pack()

typedef enum eFMSelReturn {
   kSelFMRet_OK = 0, // run selected FM 'data->sName' (0-len string to run without an FM)
   kSelFMRet_Cancel = -1, // cancel FM selection and start game as-is (no FM or if defined in cam_mod.ini use that)
   kSelFMRet_ExitGame = 1 // abort and quit game
} eFMSelReturn;

typedef int (__cdecl *FMSelectorFunc)(sFMSelectorData*);

Hoping some bilingual C++/.NET guru can help me out.

Upvotes: 0

Views: 663

Answers (2)

GSerg
GSerg

Reputation: 78134

With DllImport this structure would be

<StructLayout(LayoutKind.Sequential, Pack:=4)>
Public Structure sFMSelectorData
    Public nStructSize As Integer

    <MarshalAs(UnmanagedType.LPStr)>
    Public sGameVersion As String

    ' supplied initial FM root path (the FM Selector may change this)
    <MarshalAs(UnmanagedType.LPStr)>
    Public sRootPath As String
    Public nMaxRootLen As Integer

    ' buffer to copy the selected FM name
    <MarshalAs(UnmanagedType.LPStr)>
    Public sName As String
    Public nMaxNameLen As Integer

    ' set to non-zero when selector Is invoked after game exit (if requested during game start)
    Public bExitedGame As Integer
    ' FM selector should set this to non-zero if it wants to be invoked after game exits (only done for FMs)
    Public bRunAfterGame As Integer

    ' optional list of paths to exclude from mod_path/uber_mod_path in + separated format And Like the config
    ' vars, Or if "*" all Mod paths are excluded (leave buffer empty for no excludes)
    ' the specified exclude paths work as if they had a "*\" wildcard prefix
    <MarshalAs(UnmanagedType.LPStr)>
    Public sModExcludePaths As String
    Public nMaxModExcludeLen As Integer
End Structure

Whether that works depends on exactly what that DllExport does.

Upvotes: 1

Eissa N.
Eissa N.

Reputation: 1725

For .Net applications, you can use C++/CLI and you may find more information on MSDN. I have used C++/CLI in the past with great success for calling C++ objects in C#; although, later we used SWIG for this purpose, as we needed for Java, Python, and R too.

Upvotes: 0

Related Questions