简体   繁体   中英

P/Invoke marshaling and unmarshalling 2D array, structure and pointers between C# and unmanaged DLL

Flann C++ library has wrappers for C , C++ , Python , Matlab and Ruby but no C# wrapper available. I am trying to create a C# wrapper around flann.dll 32-bit unmanaged DLL downloaded from here .

Being new to PInvoke/marshalling, I am quite certain I am not doing the C# P/Invoke calls to the DLL correctly. I am basically trying to mirror the available Python wrapper in C#. Main areas of confusion are:

  • I am not sure how to marshal (input) and unmarshal (output) between a 2D managed rectangular array in C# where the argument type is float* ie pointer to a query set stored in row major order (according to comments in flann.h ).
  • I am also not sure how I am passing a structure reference to C is correct ie struct FLANNParameters*
  • Is IntPtr appropriate to reference typedef void* and int* indices ?

Unmanaged C (flann.dll library)

Public exported C++ methods from flann.h that I need to use are as follows:

typedef void* FLANN_INDEX; /* deprecated */
typedef void* flann_index_t;

FLANN_EXPORT extern struct FLANNParameters DEFAULT_FLANN_PARAMETERS;

// dataset = pointer to a query set stored in row major order
FLANN_EXPORT flann_index_t flann_build_index(float* dataset,
                                             int rows,
                                             int cols,
                                             float* speedup,
                                             struct FLANNParameters* flann_params);

FLANN_EXPORT int flann_free_index(flann_index_t index_id,
                                  struct FLANNParameters* flann_params);

FLANN_EXPORT int flann_find_nearest_neighbors(float* dataset,
                                              int rows,
                                              int cols,
                                              float* testset,
                                              int trows,
                                              int* indices,
                                              float* dists,
                                              int nn,
                                              struct FLANNParameters* flann_params);

Managed C# wrapper (my implementation)

Here is my C# wrapper based on the above publicly exposed methods.

NativeMethods.cs

using System;
using System.Runtime.InteropServices;

namespace FlannWrapper
{
    /// <summary>
    /// Methods to map between native unmanaged C++ DLL and managed C#
    /// Trying to mirror: https://github.com/mariusmuja/flann/blob/master/src/cpp/flann/flann.h
    /// </summary>
    public class NativeMethods
    {
        /// <summary>
        /// 32-bit flann dll obtained from from http://sourceforge.net/projects/pointclouds/files/dependencies/flann-1.7.1-vs2010-x86.exe/download
        /// </summary>
        public const string DllWin32 = @"C:\Program Files (x86)\flann\bin\flann.dll";

        /// <summary>
        /// C++: flann_index_t flann_build_index(float* dataset, int rows, int cols, float* speedup, FLANNParameters* flann_params)
        /// </summary>
        [DllImport(DllWin32, EntryPoint = "flann_build_index", CallingConvention = CallingConvention.Cdecl, SetLastError = true)]
        public static extern IntPtr flannBuildIndex([In] [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.R4)] float[,] dataset,  // ??? [In] IntPtr dataset ???
                                                    int rows, int cols, 
                                                    ref float speedup,      // ???
                                                    [In] ref FlannParameters flannParams);  // ???

        /// <summary>
        /// C++: int flann_free_index(flann_index_t index_ptr, FLANNParameters* flann_params)
        /// </summary>
        [DllImport(DllWin32, EntryPoint = "flann_free_index", CallingConvention = CallingConvention.Cdecl, SetLastError = true)]
        public static extern int flannFreeIndex(IntPtr indexPtr,        // ???
                                                [In] ref FlannParameters flannParams);   // ??? [In, MarshalAs(UnmanagedType.LPStruct)] FlannParameters flannParams);

        /// <summary>
        /// C++: int flann_find_nearest_neighbors_index(flann_index_t index_ptr, float* testset, int tcount, int* result, float* dists, int nn, FLANNParameters* flann_params)
        /// </summary>
        [DllImport(DllWin32, EntryPoint = "flann_find_nearest_neighbors_index", CallingConvention = CallingConvention.Cdecl, SetLastError = true)]
        public static extern int flannFindNearestNeighborsIndex(IntPtr indexPtr,        // ???
                                                                [In] [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.R4)] float[,] testset,  // ??? [In] IntPtr dataset ???
                                                                int tCount,
                                                                [Out] IntPtr result,    // ??? [Out] [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.R4)] int[,] result, 
                                                                [Out] IntPtr dists,     // ???
                                                                int nn,
                                                                [In] ref FlannParameters flannParams);  // ???
    }
}

FlannTest.cs

using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace FlannWrapper
{
    [TestClass]
    public class FlannTest : IDisposable
    {
        private IntPtr curIndex; 
        protected FlannParameters flannParams;
        // protected GCHandle gcHandle;

        [TestInitialize]
        public void TestInitialize()
        {
            this.curIndex = IntPtr.Zero;
            // Initialise Flann Parameters
            this.flannParams = new FlannParameters();  // use defaults
            this.flannParams.algorithm = FlannAlgorithmEnum.FLANN_INDEX_KDTREE;
            this.flannParams.trees = 8;
            this.flannParams.logLevel = FlannLogLevelEnum.FLANN_LOG_WARN;
            this.flannParams.checks = 64;
        }

        [TestMethod]
        public void FlannNativeMethodsTestSimple()
        {
            int rows = 3, cols = 5;
            int tCount = 2, nn = 3;

            float[,] dataset2D = { { 1.0f,      1.0f,       1.0f,       2.0f,       3.0f},
                                   { 10.0f,     10.0f,      10.0f,      3.0f,       2.0f},
                                   { 100.0f,    100.0f,     2.0f,       30.0f,      1.0f} };
            //IntPtr dtaasetPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(float)) * dataset2D.Length);

            float[,] testset2D = { { 1.0f,      1.0f,       1.0f,       1.0f,       1.0f},
                                   { 90.0f,     90.0f,      10.0f,      10.0f,      1.0f} };
            //IntPtr testsetPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(float)) * testset2D.Length);

            int outBufferSize = tCount * nn;
            int[] result = new int[outBufferSize];
            int[,] result2D = new int[tCount, nn];
            IntPtr resultPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(int)) * result.Length);

            float[] dists = new float[outBufferSize];
            float[,] dists2D = new float[tCount, nn];
            IntPtr distsPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(float)) * dists.Length);

            try
            {
                // Copy the array to unmanaged memory.
                //Marshal.Copy(testset, 0, testsetPtr, testset.Length);
                //Marshal.Copy(dataset, 0, datasetPtr, dataset.Length);

                if (this.curIndex != IntPtr.Zero)
                {
                    // n - number of bytes which is enough to keep any type used by function
                    NativeMethods.flannFreeIndex(this.curIndex, ref this.flannParams);
                    this.curIndex = IntPtr.Zero;
                }

                //GC.KeepAlive(this.curIndex);    // TODO

                float speedup = 0.0f;  // TODO: ctype float

                Console.WriteLine("Computing index.");
                this.curIndex = NativeMethods.flannBuildIndex(dataset2D, rows, cols, ref speedup, ref this.flannParams);
                NativeMethods.flannFindNearestNeighborsIndex(this.curIndex, testset2D, tCount, resultPtr, distsPtr, nn, ref this.flannParams);

                // Copy unmanaged memory to managed arrays.
                Marshal.Copy(resultPtr, result, 0, result.Length);
                Marshal.Copy(distsPtr, dists, 0, dists.Length);

                // Clutching straws, convert 1D to 2D??
                for(int row=0; row<tCount; row++)
                {
                    for(int col=0; col<nn; col++)
                    {
                        int buffIndex = row*nn + col;
                        result2D[row, col] = result[buffIndex];
                        dists2D[row, col] = dists[buffIndex];
                    }
                }
            }
            finally
            {
                // Free unmanaged memory -- [BREAKPOINT HERE]
                // Free input pointers
                //Marshal.FreeHGlobal(testsetPtr);
                //Marshal.FreeHGlobal(datasetPtr);
                // Free output pointers
                Marshal.FreeHGlobal(resultPtr);
                Marshal.FreeHGlobal(distsPtr);
            }
        }

        [TestCleanup]
        public void TestCleanup()
        {
            if (this.curIndex != IntPtr.Zero)
            {
                NativeMethods.flannFreeIndex(this.curIndex, ref flannParams);
                Marshal.FreeHGlobal(this.curIndex);
                this.curIndex = IntPtr.Zero;
                // gcHandle.Free();
            }
        }
    }
}

FlannParams.cs

Trying to mirror Python FLANNParameters class and C struct FLANNParameters .

using System;
using System.Runtime.InteropServices;

namespace FlannWrapper
{
    // FieldOffsets set based on assumption that C++ equivalent of int, uint, float, enum are all 4 bytes for 32-bit
    [StructLayout(LayoutKind.Explicit)]
    public class FLANNParameters
    {
        [FieldOffset(0)]
        public FlannAlgorithmEnum algorithm;
        [FieldOffset(4)]
        public int checks;
        [FieldOffset(8)]
        public float eps;
        [FieldOffset(12)]
        public int sorted;
        [FieldOffset(16)]
        public int maxNeighbors;
        [FieldOffset(20)]
        public int cores;
        [FieldOffset(24)]
        public int trees;
        [FieldOffset(28)]
        public int leafMaxSize;
        [FieldOffset(32)]
        public int branching;
        [FieldOffset(36)]
        public int iterations;
        [FieldOffset(40)]
        public FlannCentersInitEnum centersInit;
        [FieldOffset(44)]
        public float cbIndex;
        [FieldOffset(48)]
        public float targetPrecision;
        [FieldOffset(52)]
        public float buildWeight;
        [FieldOffset(56)]
        public float memoryWeight;
        [FieldOffset(60)]
        public float sampleFraction;
        [FieldOffset(64)]
        public int tableNumber;
        [FieldOffset(68)]
        public int keySize;
        [FieldOffset(72)]
        public int multiProbeLevel;
        [FieldOffset(76)]
        public FlannLogLevelEnum logLevel;
        [FieldOffset(80)]
        public long randomSeed;

        /// <summary>
        /// Default Constructor
        /// Ref https://github.com/mariusmuja/flann/blob/master/src/python/pyflann/flann_ctypes.py : _defaults
        /// </summary>
        public FlannParameters()
        {
            this.algorithm = FlannAlgorithmEnum.FLANN_INDEX_KDTREE;
            this.checks = 32;
            this.eps = 0.0f;
            this.sorted = 1;
            this.maxNeighbors = -1;
            this.cores = 0;
            this.trees = 1;
            this.leafMaxSize = 4;
            this.branching = 32;
            this.iterations = 5;
            this.centersInit = FlannCentersInitEnum.FLANN_CENTERS_RANDOM;
            this.cbIndex = 0.5f;
            this.targetPrecision = 0.9f;
            this.buildWeight = 0.01f;
            this.memoryWeight = 0.0f;
            this.sampleFraction = 0.1f;
            this.tableNumber = 12;
            this.keySize = 20;
            this.multiProbeLevel = 2;
            this.logLevel = FlannLogLevelEnum.FLANN_LOG_WARN;
            this.randomSeed = -1;
        }
    }
    public enum FlannAlgorithmEnum  : int   
    {
        FLANN_INDEX_KDTREE = 1
    }
    public enum FlannCentersInitEnum : int
    {
        FLANN_CENTERS_RANDOM = 0
    }
    public enum FlannLogLevelEnum : int
    {
        FLANN_LOG_WARN = 3
    }
}

Incorrect Output - Debug mode, Immediate Window

?result2D
{int[2, 3]}
    [0, 0]: 7078010
    [0, 1]: 137560165
    [0, 2]: 3014708
    [1, 0]: 3014704
    [1, 1]: 3014704
    [1, 2]: 48
?dists2D
{float[2, 3]}
    [0, 0]: 2.606415E-43
    [0, 1]: 6.06669328E-34
    [0, 2]: 9.275506E-39
    [1, 0]: 1.05612418E-38
    [1, 1]: 1.01938872E-38
    [1, 2]: 1.541428E-43

As you can see, I don't get any errors when running Test in Debug mode, but I know the output is definitely incorrect - garbage values as a result of improper memory addressing. I have also included alternative marshaling signatures I tried without any success (please see comments with ???).

Ground truth Python (calling PyFlann library)

To find out the correct result, I implemented a quick test using the available Python library - PyFlann.

FlannTest.py

import pyflann
import numpy as np

dataset = np.array(
    [[1., 1., 1., 2., 3.],
     [10., 10., 10., 3., 2.],
     [100., 100., 2., 30., 1.] ])
testset = np.array(
    [[1., 1., 1., 1., 1.],
     [90., 90., 10., 10., 1.] ])
flann = pyflann.FLANN()
result, dists = flann.nn(dataset, testset, num_neighbors = 3, 
                         algorithm="kdtree", trees=8, checks=64)  # flann parameters

# Output
print("\nResult:")
print(result)
print("\nDists:")
print(dists)

Under the hood, PyFlann.nn() calls the publicly exposed C methods as we can tell from looking at index.py .

Correct Output

Result:
[[0 1 2]
 [2 1 0]]

Dists:
[[  5.00000000e+00   2.48000000e+02   2.04440000e+04]
 [  6.64000000e+02   1.28500000e+04   1.59910000e+04]]

Any help on the correct way to do this would be greatly appreciated. Thanks.

When you're working with p/invoke, you have to stop thinking "managed", and instead think physical binary layout, 32 vs 64 bit, etc. Also, when the called native binary always runs in-process (like here, but with COM servers it can be different) it's easier than out-of-process because you don't have to think too much about marshaling/serialization, ref vs out, etc.

Also, you don't need to tell .NET what it already knows. An array of float is an LPArray of R4, you don't have to specify it. The simpler the better.

So, first of all flann_index_t . It's defined in C as void * , so it must clearly be an IntPtr (an opaque pointer on "something").

Then, structures. Structures passed as a simple pointer in C can just be passed as a ref argument in C# if you define it as struct . If you define it as a class , don't use ref . In general I prefer using struct for C structures.

You'll have to make sure the structure is well defined. In general, you use the LayoutKind.Sequential because .NET p/invoke will pack arguments the same way that the C compiler does. So you don't have to use explicit, especially when arguments are standard (not bit things) like int, float, So you can remove all FieldOffset and use LayoutKind.Sequential if all members are properly declared... but this is not the case.

For types, like I said, you really have to think binary and ask yourself for each type you use, what's its binary layout, size? int are (with 99.9% C compilers) 32-bit. float and double are IEEE standards, so there should never be issues about them. Enums are in general based on int , but this may vary (in C and in .NET, to be able to match C). long are (with 99.0% C compilers) 32-bit, not 64-bit. So the .NET equivalent is Int32 (int), not Int64 (long).

So you should correct your FlannParameters structure and replace the long by an int . If you really want to make sure for a given struct, check Marshal.SizeOf(mystruct) against C's sizeof(mystruct) with the same C compiler than the one that was used to compile the library you're calling. They should be the same. If they're not, there's an error in the .NET definition (packing, member size, order, etc.).

Here are modified definitions and calling code that seem to work.

static void Main(string[] args)
{
    int rows = 3, cols = 5;
    int tCount = 2, nn = 3;

    float[,] dataset2D = { { 1.0f,      1.0f,       1.0f,       2.0f,       3.0f},
                           { 10.0f,     10.0f,      10.0f,      3.0f,       2.0f},
                           { 100.0f,    100.0f,     2.0f,       30.0f,      1.0f} };

    float[,] testset2D = { { 1.0f,      1.0f,       1.0f,       1.0f,       1.0f},
                           { 90.0f,     90.0f,      10.0f,      10.0f,      1.0f} };

    var fparams = new FlannParameters();
    var index = NativeMethods.flannBuildIndex(dataset2D, rows, cols, out float speedup, ref fparams);

    var indices = new int[tCount, nn];
    var idists = new float[tCount, nn];
    NativeMethods.flannFindNearestNeighborsIndex(index, testset2D, tCount, indices, idists, nn, ref fparams);
    NativeMethods.flannFreeIndex(index, ref fparams);
}

[DllImport(DllWin32, EntryPoint = "flann_build_index", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr flannBuildIndex(float[,] dataset,
                                            int rows, int cols,
                                            out float speedup, // out because, it's and output parameter, but ref is not a problem
                                            ref FlannParameters flannParams);

[DllImport(DllWin32, EntryPoint = "flann_free_index", CallingConvention = CallingConvention.Cdecl)]
public static extern int flannFreeIndex(IntPtr indexPtr,  ref FlannParameters flannParams);

[DllImport(DllWin32, EntryPoint = "flann_find_nearest_neighbors_index", CallingConvention = CallingConvention.Cdecl, SetLastError = true)]
public static extern int flannFindNearestNeighborsIndex(IntPtr indexPtr,
                                                        float[,] testset,
                                                        int tCount,
                                                        [In, Out] int[,] result, // out because it may be changed by C side
                                                        [In, Out] float[,] dists,// out because it may be changed by C side
                                                        int nn,
                                                        ref FlannParameters flannParams);

[StructLayout(LayoutKind.Sequential)]
public struct FlannParameters
{
    public FlannAlgorithmEnum algorithm;
    public int checks;
    public float eps;
    public int sorted;
    public int maxNeighbors;
    public int cores;
    public int trees;
    public int leafMaxSize;
    public int branching;
    public int iterations;
    public FlannCentersInitEnum centersInit;
    public float cbIndex;
    public float targetPrecision;
    public float buildWeight;
    public float memoryWeight;
    public float sampleFraction;
    public int tableNumber;
    public int keySize;
    public int multiProbeLevel;
    public FlannLogLevelEnum logLevel;
    public int randomSeed;
}

note: I've tried to use the specific flann parameters values, but the library crashes in this case, I don't know why...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM