Search code examples
c#c++dllinterop

How to fix C++ DLL not updating data for pointer to C# struct


This is my first time working with trying to get interoperability working for C++ and C#, so I am likely making a lot of mistakes, however I have been researching the topic for about a week now and unable to get past this final hurdle.

The Scenario:

I am attempting to have a C++ application that is generating values pass those values into a struct inside a C++ DLL, then pass that data inside the struct to a WinForms GUI made in C# (the decision for C# is that I am much more familiar with it, and thus am more comfortable working in it, plus the ease of use in designing and working with the GUI) to then use that data on the C# side to update some visual element (i.e., text boxes) so that a user can see what the values being generated by the C++ application are.

The Current Situation:

I have successfully built a C++ DLL and have been able to call it in both the C++ application that is generating data and the C# application that is supposed to translate that data into the visual elements. I have confirmed that the C# application is able to call the DLL methods and functions without error, and likewise on the C++ application side. There are not errors in importing the methods or calling them.

On the C++ application side of things, things seem to be working perfectly fine. However, on the C# side of things, for whatever reason, the struct inside the DLL is not updating values correctly in order to pass that data on to a pointer to the equivalent struct on the C# side. The pointer is updating to something, as I can get the pointer to show the same values as are present in the initialization of the DLL struct (will give better detail below).

The DLL has two instances of the same struct, one being exported (which is used in the C++ application) and one that is intended to be used internally (it is declared in the header for the DLL as extern and then defined in the .cpp file for the DLL). The struct has the following declaration and definition in the header file for the DLL (the below block will be the entire header file):

#pragma once

#define DLL_EXPORT extern "C" __declspec(dllexport)

typedef struct TestDataStruct {

    int test_int_1 = 57;
    int test_int_2 = 0;
    double test_double_1 = 0.0;
    double test_double_2 = 0.0;
    const char* test_const_char_arr;

} TestDataType;

extern TestDataType testDataType;

DLL_EXPORT void UpdateData(TestDataType* testData);

DLL_EXPORT TestDataType UpdateTestDataType();

DLL_EXPORT void UpdatePointerData(TestDataType* testData_cs);

DLL_EXPORT void PrintTestData();

NOTE: The int test_int_1 = 57; is a temporary assignment to an arbitrary number just to ensure that some data was being read on the C# side of things, and is not the actual intended value for that struct. Also, I am aware that const char* is not a char array, though at the time of writing the struct, I believed it behaved the same way, hence the name.

In the DLL .cpp file, I have the following:

#include "TestDataType.h"
#include <iostream>

#define DLL_EXPORT extern "C" __declspec(dllexport)

DLL_EXPORT TestDataType test_data = TestDataType{0,0,0.0,0.0,""};

TestDataType testDataType;

void UpdateData(TestDataType* testData) {

    std::cout << "RECEIVED CALL TO UPDATE VALUES!" << std::endl;

    testDataType.test_int_1 = testData->test_int_1;
    testDataType.test_int_2 = testData->test_int_2;
    testDataType.test_double_1 = testData->test_double_1;
    testDataType.test_double_2 = testData->test_double_2;
    testDataType.test_const_char_arr = testData->test_const_char_arr;

}

void UpdatePointerData(TestDataType* testData_cs) {

    std::cout << "BEFORE CHANGING: " << testData_cs->test_int_1 << std::endl;

    testData_cs->test_int_1 = testDataType.test_int_1;

    std::cout << "AFTER CHANGING: " << testData_cs->test_int_1 << std::endl;

}

void PrintTestData() {
     
    std::cout << "CALLING FROM PRINTTESTDATA()... " << std::endl;
    std::cout << "Printing Test Data..." << std::endl;
    std::cout << "Int 1: " << testDataType.test_int_1 << std::endl;
    std::cout << "Int 2: " << testDataType.test_int_2 << std::endl;
    std::cout << "Double 1: " << testDataType.test_double_1 << std::endl;
    std::cout << "Double 2: " << testDataType.test_double_2 << std::endl;
    std::cout << "Char Arr: " << testDataType.test_const_char_arr << std::endl;

}

NOTE: The UpdateData() function and test_data object are used ONLY in the C++ Application, the PrintTestData() was intended to be used in both the C++ and C# applications, but is currently only used in the C++ application, and all other functions are only used in the C# application.

As mentioned above, the C++ side of things works perfectly fine and as desired (though, odd to me, when calling the DLL functions from the C++ application, they output their prints to the same console of the C++ application, though I am not sure if this is correct behavior or not as, again, I have never tried to do this before). The C++ application has a single header and its associated .cpp file:

.h
#pragma once
#include "..\TestDLL\TestDataType.h"

void GenerateRandomValues(TestDataType* testData);
int GenerateRandomInt();
double GenerateRandomDouble();
const char* GenerateRandomString();
.cpp
#include <iostream>
#include <random>
#include <Windows.h>
#include "CPP_CS_Interop_Test_CPP.h"

int main()
{
    HMODULE testdll_lib = LoadLibrary(L"TestDLL.dll");

    if (testdll_lib != NULL) {

        typedef void(__cdecl* UpdateData)(TestDataType*);
        typedef void(__cdecl* PrintTestData)(void);

        UpdateData updateVals = (UpdateData)GetProcAddress(testdll_lib, "UpdateData");
        TestDataType* test_data = (TestDataType*)GetProcAddress(testdll_lib, "test_data");
        PrintTestData printData = (PrintTestData)GetProcAddress(testdll_lib, "PrintTestData");

        if (!updateVals) {

            std::cerr << "Failed to get function address" << std::endl;
            FreeLibrary(testdll_lib);
            std::cin.get();
            return 1;

        }

        if (!printData) {

            std::cerr << "Failed to get function address" << std::endl;
            FreeLibrary(testdll_lib);
            std::cin.get();
            return 1;

        }

        if (!test_data) {

            std::cerr << "Failed to get struct address" << std::endl;
            FreeLibrary(testdll_lib);
            std::cin.get();
            return 1;

        }

        for(int i = 0; i < 1000; i++) {


            GenerateRandomValues(test_data);


            std::cout << "PRINTING BEFORE UPDATE DATA..." << std::endl;
            printData();

            updateVals(test_data);

            std::cout << "PRINTING AFTER UPDATE DATA..." << std::endl;
            printData();

        }

        FreeLibrary(testdll_lib);
    
    }
    else {
    
        std::cerr << "Failed to load DLL" << std::endl;
        std::cin.get();
        return 1;
    
    }

}

NOTE: I have not included the GenerateRandomValues method and the methods it calls because they are irrelevant to the problem at hand.

On the C# side of things, I have been able to confirm that at least something is being set, once, because the C# side, when calling the UpdatePointerData DLL function shows that the "BEFORE CHANGING" cout is 0, and the "AFTER CHANGING" cout is 57 (this is also reflected in the UpdateValues method in C# that will be defined below), but the problem arises that the C# side is not being updated beyond this initial set. The C# files are below (NOTE: I am not including the main C# file, as all it does it run the WinForm code):

struct:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace CPP_CS_Interop_Test_CS
{

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    public struct TestDataType
    {

        public int test_int_1 ;
        public int test_int_2;
        public double test_double_1;
        public double test_double_2;
        public string test_string;

    }


}
Form:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using static System.Net.Mime.MediaTypeNames;

namespace CPP_CS_Interop_Test_CS
{
    public partial class Form1 : Form
    {

        [DllImport("TestDLL.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern void UpdatePointerData(ref TestDataType testData);

        [DllImport("TestDLL.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern void PrintTestData();

        public TestDataType testData_cs;

        public Form1()
        {
            InitializeComponent();

        }
        public void UpdateValues()
        {

            Console.WriteLine($"Print testData_cs test_int_1 before update: {testData_cs.test_int_1}");

            UpdatePointerData(ref testData_cs);

            Console.WriteLine($"Print testData_cs test_int_1 after update: {testData_cs.test_int_1}");

        }

        private void Form1_Load(object sender, EventArgs e)
        {

            Timer timer = new Timer();
            timer.Interval = 10;
            timer.Tick += new EventHandler(timer_tick);
            timer.Start();

        }

        private void timer_tick(object sender, EventArgs e)
        {
            UpdateValues();
        }
    }
}

Including some sample output for each application:

C++ Application:
PRINTING BEFORE UPDATE DATA...
CALLING FROM PRINTTESTDATA()...
Printing Test Data...
Int 1: 57
Int 2: 0
Double 1: 0
Double 2: 0
Char Arr: This is a test string that only serves the purpose of having something here.
RECEIVED CALL TO UPDATE VALUES!
PRINTING AFTER UPDATE DATA...
CALLING FROM PRINTTESTDATA()...
Printing Test Data...
Int 1: 6419
Int 2: 3266
Double 1: 711.276
Double 2: 2255.51
Char Arr:
C# Application:
Print testData_cs test_int_1 before update: 0
BEFORE CHANGING: 0
AFTER CHANGING: 57
Print testData_cs test_int_1 after update: 57
Print testData_cs test_int_1 before update: 57
BEFORE CHANGING: 57
AFTER CHANGING: 57
Print testData_cs test_int_1 after update: 57

As can be seen, the C# data does get updated, once, but does not continue to get updated, and this is where I am stuck at trying to figure this out. Is there some sort of deallocation happening where the struct inside the DLL that is supposed to update the C# struct is not being kept in memory, so it keeps instantiating it as if it's a new struct again?

I am beyond being able to figure this issue out without help, despite all of the research I have poured into this, and would really like some clarification if nothing else. As mentioned, this is my first time trying this type of interoperability, so I have no idea if I am just doing some absolutely asinine things, so please feel free to correct mistakes I am making, but I honestly have no idea what I am doing wrong.

I have tried doing independent research into how this interoperability works, through reading various articles and tutorials, reviewing AI generated code explanations (I know that this is not always a good idea, but I was trying everything I could think of), reading other questions and answers I could find here on Stack Overflow, all to no avail.

The only thing I can surmise is different from what I am doing versus anyone else is that I am trying to send the values from a declaration of a global variable in the DLL back to the C# struct, but I do not know if this is a problem, since the C++ application seems to be doing this without issue.

I have tried so many various iterations of handling for this that I honestly cannot remember them all to try to include them here, but even after attempting to follow video tutorials, I have not had much luck, as most of the videos I have found simply involve using the DLL to create the struct (i.e., initialize the data) and pass that back to the C# side or attempt to pass a struct from C# to the DLL in order to run a function of the DLL struct on the one passed in from the C# side, and none I have found seem to try to pass the data back and forth while it is being updated elsewhere.

As for what I would like to get working, I simply need to be able to get the data from the DLL that the C++ application is generating and passing into the DLL into the C# struct. If there is a more concise way of doing this, so long as it works, I am more than happy to rewrite this entirely if it means it will work, as this is just a test project to get familiar with the concepts before I try to implement these ideas into a different, larger project.

For any explanations or answers provided, please provide as much detail as possible so I can try to learn exactly what I am doing wrong and why it is wrong (if this post isn't obvious, I clearly appreciate what many would call "too much information" as opposed to not enough information.

NOTE: To add some clarification, all three projects are able to build fine together, the C++ application and DLL are both Win32, and the C# project is running on VS 2022's "Any CPU" platform setting, which matches what will be required in the larger project these concepts are meant to be applied to.


Solution

  • You are not seeing the values being updated on the C# side after the 1st call, simply because you are reusing the same testData_cs variable for subsequent calls to UpdatePointerData() but you never clear that variable's data, and you never call UpdateData() to change the data that is inside the DLL. So it just writes out the same values every time UpdatePointerData() is called.

    Keep in mind that your C++ app and your C# app are going to be using separate instances of your DLL and its internal data. Since your goal is to have your C++ app generate data that your C# app consumes, the code you have shown will not work. You would need to copy the data from the C++ app's instance of the DLL to the C# app's instance of the DLL, and vice versa.

    One way to handle that is to move the DLL's internal struct into shared memory so that both instances of the DLL can access it directly.

    How do I share data in my DLL with an application or with other DLLs?

    Win32 DLLs are mapped into the address space of the calling process. By default, each process using a DLL has its own instance of all the DLLs global and static variables. If your DLL needs to share data with other instances of it loaded by other applications, you can use either of the following approaches:

    • Create named data sections using the data_seg pragma.

    • Use memory mapped files.

    If you are using Visual C++, you can use #pragma data_seg() at compile-time, eg:

    #pragma data_seg (".myseg")
    TestDataType testDataType;
    #pragma data_seg()
    

    Otherwise, you can use CreateFileMapping() and MapViewOfFile() at runtime (see Creating Named Shared Memory), eg:

    HANDLE hTestDataMap = NULL;
    TestDataType* pTestDataType = NULL;
    
    BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
    {
        switch (fdwReason)
        { 
            case DLL_PROCESS_ATTACH:
                hTestDataMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(TestDataType), TEXT("MyTestDataType"));
                if (hTestDataMap == NULL)
                    return FALSE;
    
                pTestDataType = (TestDataType*) MapViewOfFile(hTestDataMap, FILE_MAP_ALL_ACCESS, 0, 0, TestDataType));
                if (pTestDataType == NULL)
                    return FALSE;
    
                break;
    
            case DLL_PROCESS_DETACH:
                if (pTestDataType != NULL)
                    UnmapViewOfFile(pTestDataType);
                if (hTestDataMap != NULL)
                    CloseHandle(hTestDataMap);
                break;
        }
    
        return TRUE;
    }
    
    void UpdateData(TestDataType* testData) {
    
        std::cout << "RECEIVED CALL TO UPDATE VALUES!" << std::endl;
    
        pTestDataType->test_int_1 = testData->test_int_1;
        pTestDataType->test_int_2 = testData->test_int_2;
        pTestDataType->test_double_1 = testData->test_double_1;
        pTestDataType->test_double_2 = testData->test_double_2;
        pTestDataType->test_const_char_arr = testData->test_const_char_arr;
    
    }
    
    void UpdatePointerData(TestDataType* testData_cs) {
    
        std::cout << "BEFORE CHANGING: " << testData_cs->test_int_1 << std::endl;
    
        testData_cs->test_int_1 = pTestDataType->test_int_1;
    
        std::cout << "AFTER CHANGING: " << testData_cs->test_int_1 << std::endl;
    
    }
    
    void PrintTestData() {
         
        std::cout << "CALLING FROM PRINTTESTDATA()... " << std::endl;
        std::cout << "Printing Test Data..." << std::endl;
        std::cout << "Int 1: " << pTestDataType->test_int_1 << std::endl;
        std::cout << "Int 2: " << pTestDataType->test_int_2 << std::endl;
        std::cout << "Double 1: " << pTestDataType->test_double_1 << std::endl;
        std::cout << "Double 2: " << pTestDataType->test_double_2 << std::endl;
        std::cout << "Char Arr: " << pTestDataType->test_const_char_arr << std::endl;
    
    }
    

    On a side note -

    On the C# side, you should get rid of Pack = 1 since your C++ struct is not using 1-byte alignment. And also, change the test_string field to be an IntPtr that you marshal manually with Marshal.StringToHGlobalAnsi() or equivalent.

    Also keep in mind that you are pointing the DLL's test_const_char_arr field to point at memory inside of the calling app, so make sure the app does not free that memory while the DLL is holding a pointer to it. All the more reason why you need manual marshaling on the C# side in particular (especially since you are converting from Unicode to Ansi), unless you pin the string data to prevent .NET from moving it around.