[Mono-list] need some help with PInvoke..

Jonathan Pryor jonpryor@vt.edu
09 Jul 2003 14:15:09 -0400


I suspect that what you want to do isn't possible.

You state that you're using opaque data types.  So, to paraphrase a lot,
you have an API like:

	/* C language */
	typedef void* HDATA;
	char* GetDescription (HDATA data);
	void CreateData (HDATA* to_init);
	/* ... */

This doesn't have an exact "mesh" with P/Invoke.  There are two ways to
wrap something like the above.

The first way is similar to what's done in Gtk# -- use System.IntPtr
instead of "void*" and use the IntPtr exclusively as a pointer into
managed memory:

	// C#
	public class Data {
		[DllImport ("mylib")]
		private static extern string GetDescription(IntPtr d);

		[DllImport ("mylib")]
		private static extern void CreateData (out IntPtr init);

		private IntPtr handle;

		public Data () {
			CreateData (out handle);
		}

		public string Description {
			get {return GetDescription (handle);}
		}
	}

This works, but has IntPtr's everywhere.

The second method would be to just use unsafe code everywhere, and use
void* as the data member instead of a IntPtr.  Otherwise, it looks the
same.

However, what you posted doesn't exactly match the above.  You have a
"mostly opaque" data structure (given that your C code directly
references the `desc' member of the NOERR class).

There's a problem with this.  The .NET/mono runtime systems make two
assumptions: structs are located on the stack, and classes are allocated
in garbage-collected, .NET-controlled, memory.  Non-stack, non-garbage
collected memory doesn't enter the picture AT ALL.  This runs directly
contrary to your example code, where `hdf_init' is allocating
(unmanaged) memory and storing a pointer to it in *hdf.  You're trying
to mix the two memory heaps (managed vs. unmanaged), and the runtime
won't like that.

For example, if we assume the C code:

	void hdf_init (HDF** hdf) {
		*hdf = malloc (sizeof(HDF));
	}

and the C# code:

	class Hdf {
		int foo;
	}

	class User {
		[DllImport("")]
		private static extern void hdf_init (ref Hdf);

		public static void Main () {
			Hdf h = null;
			hdf_init (ref h);
		}
	}

Recall that .NET class variables are actually pointers, so the argument
for `User.hdf_init' is correct -- a pointer to a pointer is actually
passed.  The problem is the interaction between the C code, and how it
breaks the .NET memory model assumptions.  After the call to hdf_init,
`h' will actually refer to unmanaged, non-GCed memory.  FURTHERMORE,
what the memory is actually pointing to is no longer a .NET `Hdf' type. 
.NET class types contain extra object header information, for use with
run-time type checking and the virtual function mechanism.  So if you
attempt to use this, you can either expect an error on the first use of
`h', or an error/segfault on the next garbage collection.

In short, you can't do that.  Mixing managed and unmanaged memory can
only really be done in Managed C++, and you have to be *very* careful
when you do so (paying careful attention to the issues outlined above).

So, what's the solution?  Expose your HDF type as a *fully* opaque data
type, providing C functions to access whatever data is within it,
instead of using "normal" member access.  Use the accessor functions
from C#. and make sure you keep the managed and unmanaged memory
separate.

See the generated Gtk# sources for an example of this.  C code can
directly access GObject members, but (1) that's not good style (you're
not supposed to do that), and (2) it's impossible to do that from C#.

 - Jon

P.S.  Sorry for the long email/rant.  Also, don't try actually compiling
the code printed above -- I didn't try compiling it, it's for
demonstration/example purposes only (to help make my point). 

On Wed, 2003-07-09 at 03:37, David Jeske wrote:
> I'm trying to write a PInvoke wrapper and I'm having trouble doing
> something pretty basic. Perhaps someone can help me.
> 
> The C-function I'm trying to call has this prototype:
> 
>   NEOERR* hdf_init (HDF **hdf);
> 
> As far as C# is concerned, HDF and NEOERR are just opaque
> data-structures. 
> 
> When hdf_init runs, it allocates some datastructures, and passes back
> the pointer through the externally allocated hdf pointer. I just want
> to print the address of this structure so I can see that something is
> working. Here is a C-program that does what I want..
> 
> #include <ClearSilver.h>
> 
> int main() {
>   HDF *hdf;
>   NEOERR *err;
> 
>   err = hdf_init(&hdf);
> 
>   if (err) {
>       printf("error: %s\n", err->desc);
>       return 1;
>    }
> 
>    printf("success: 0x%X\n", hdf);
> }
> 
> I can't seem to get this to work through C# and PInvoke with
> mono.. (All of this is with Linux. I can't try with csc right now
> because I don't have the dll built for windows) My attempt is here:
> 
> using System;
> using System.Runtime.InteropServices;
> 
> [StructLayout(LayoutKind.Sequential)] public class HDF {}
> [StructLayout(LayoutKind.Sequential)] public class LPHDF {
>   //[MarshalAs(UnmanagedType.LPStruct)]
>   //public HDF hdf;
>   public int hdf;
> }
> 
> public class Hdf {
>   // NEOERR* hdf_init (_HDF **hdf);
> 
>   [DllImport("libneo.so", EntryPoint="hdf_init")]
>   [return: MarshalAs(UnmanagedType.LPStruct)]
>   private static extern void hdf_init(
>        [ MarshalAs(UnmanagedType.LPStruct)]
>          LPHDF hdf);
> 
> public static int Main(string[] argv) {
>    Console.WriteLine("start test");
> 
>     LPHDF lphdf = new LPHDF();
>     hdf_init(lphdf);
>     Console.WriteLine(lphdf.hdf);
>    
> 
>    return 0;
>  }
> 
> };
> 
> Whenever I run this, it just prints out "0" as the alleged value of
> lphdf.hdf. This is no good. Any suggestions?
> 
> If you want to poke at the real stuff, you can download libneo.so here:
> 
>   http://mozart.chat.net/~jeske/_drop/pinvoke/
> 
> libneo is built out of the clearsilver package from here:
>  
>   http://www.clearsilver.net/