Question

When I googled around I saw posts saying that passing a C# class is the same as passing struct by reference (i.e. a ref SomeStruct name parameter) to a C API while using PInvoke (here is one the posts C# PInvoke struct vs class access violation).

However when running an example I am seeing a different behavior than expected. Where a ref to a struct acts as a real pointer while 'class' does not

C code :

//PInvokeProvider.h
#include "stdafx.h" 
typedef struct Animal_s
{
    char Name[10000];
} Animal;

extern "C" void __declspec(dllexport) ChangeName(Animal* pAnimal);


//PInvokeProvider.cpp    
#include "stdafx.h"
#include <stdio.h>
#include "PInvokeProvider.h"

extern "C" {
    void ChangeName(Animal* pAnimal)
    {
        printf("Entered C++\n");
        printf("Recieved animal : %s\n", pAnimal->Name);
        printf("This function will change the first letter of animal to 'A'\n");
        pAnimal->Name[0] = 'A';
        printf("Animal changed to : %s\n", pAnimal->Name);
        printf("Leaving C++\n");
    }
}

Now onto C# using struct for `Animal':

namespace PInvokeConsumer
{
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct Animal
    {
        /// char[10000]
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 10000)]
        public string Name;

        public Animal(string name)
        {
            Name = name;
        }
    }

    public partial class NativeMethods
    {
        [DllImportAttribute("PInvokeProvider.dll", 
                            EntryPoint = "ChangeName", 
                            CallingConvention = CallingConvention.Cdecl)]
        public static extern void ChangeName(ref Animal pAnimal);
    }

    internal class Program
    {
        public static void Main(string[] args)
        {
            Animal animal = new Animal("Lion");

            Console.WriteLine("Animal : {0}", animal.Name);

            Console.WriteLine("Leaving C#");
            NativeMethods.ChangeName(ref animal);
            Console.WriteLine("Back to C#");

            Console.WriteLine("Animal : {0}", animal.Name);
            Console.ReadKey();
        }
    }
}

The output using a ref to a struct is as expected changing the the first letter to 'A' in C# realm:

Animal : Lion
Leaving C#
Entered C++
Recieved animal : Lion
This function will change the first letter of animal to 'A'
Animal changed to : Aion
Leaving C++
Back to C#
Animal : Aion

However when I try to use class instead of a ref to a struct:

namespace PInvokeConsumer
{
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public class Animal
    {
        /// char[10000]
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 10000)]
        public string Name;

        public Animal(string name)
        {
            Name = name;
        }
    }

    
    public partial class NativeMethods
    {
        [DllImportAttribute("PInvokeProvider.dll", 
                            EntryPoint = "ChangeName", 
                            CallingConvention = CallingConvention.Cdecl)]
        public static extern void ChangeName(Animal pAnimal);
    }

    public static void Main(string[] args)
    {
        Animal animal = new Animal("Lion");

        Console.WriteLine("Animal : {0}", animal.Name);

        Console.WriteLine("Leaving C#");
        NativeMethods.ChangeName(animal);
        Console.WriteLine("Back to C#");

        Console.WriteLine("Animal : {0}", animal.Name);
        Console.ReadKey();
    }
}

The output is:

Animal : Lion
Leaving C#
Entered C++
Recieved animal : Lion
This function will change the first letter of animal to 'A'
Animal changed to : Aion
Leaving C++
Back to C#
Animal : Lion

So when I use a class the memory allocated to Animal does not get modified. The question is why not?

Was it helpful?

Solution

  [StructLayout(LayoutKind.Sequential, ...)]

This is what matters. You had to apply that attribute to the class declaration. You also applied it to the struct declaration but that wasn't actually necessary, the CLR assumes it as the default for struct declarations. Modulo the CharSet property.

But the default [StructLayout] attribute for classes is not LayoutKind.Sequential, it is LayoutKind.Auto. Which is something the CLR takes advantage of, it will reorganize the fields in a class to come up with the best possible layout. You can read more about it in this post.

The big difference is that it makes a class non-blittable. Which is a hundred dollar word that means that the pinvoke marshaller cannot just pass a plain pointer to the managed object, it has to convert the managed object to an unmanaged one that has the requested layout. It does so by allocating memory and copying the fields. With the consequence that any changes that the native code makes to the copy doesn't get propagated back to the original managed object.

Unless you explicitly tell the pinvoke marshaller to do this. Fix:

    [DllImportAttribute(...)]
    public static extern void ChangeName([In, Out]Animal pAnimal);

The [OutAttribute] tells it to propagate changes back.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top