c# - P/Invoke 在 C# 和非托管 DLL 之间编码和解码二维数组、结构和指针

标签 c# python c++ pinvoke marshalling

Flann C++ library 具有 CC++PythonMatlabRuby 的包装器,但没有可用的 C# 包装器。我正在尝试围绕从 here 下载的 flann.dll 32 位非托管 DLL 创建一个 C# 包装器。

作为 PInvoke/编码的新手,我很确定我没有正确执行对 DLL 的 C# P/Invoke 调用。我基本上是在尝试在 C# 中镜像可用的 Python wrapper。混淆的主要领域是:

  • 我不确定如何在参数类型为 float* 的 C# 二维托管矩形数组之间编码(输入)和解码(输出),即指向存储在 row major order 中的查询集的指针(根据 flann.h 中的评论)。
  • 我也不确定我如何将结构引用传递给 C 是正确的,即 struct FLANNParameters*
  • IntPtr 是否适合引用 typedef void*int* indices

非托管 C(flann.dll 库)

我需要使用的从flann.h公开导出的C++方法如下:

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);

托管 C# 包装器(我的实现)

这是我基于上述公开方法的 C# 包装器。

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

尝试镜像 Python FLANNParameters classC 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
    }
}

不正确的输出 - Debug模式,立即窗口

?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

如您所见,在 Debug模式下运行测试时我没有收到任何错误,但我知道输出肯定是不正确的 - 内存寻址不当导致的垃圾值。我还包括了我尝试过但没有成功的替代编码(marshal)处理签名(请参阅带有 ??? 的评论)。

Ground truth Python(调用PyFlann库)

为了找出正确的结果,我使用可用的 Python 库 - 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)

在幕后,PyFlann.nn() 调用公开公开的 C 方法,正如我们通过查看 index.py 可以看出的那样。

正确输出

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]]

如果您能以正确的方式执行此操作,我们将不胜感激。谢谢。

最佳答案

当您使用 p/invoke 时,您必须停止考虑“托管”,而是考虑物理二进制布局、32 位与 64 位等。此外,当被调用的 native 二进制文件始终在进程中运行时(例如在这里,但对于 COM 服务器,它可能会有所不同)它比进程外更容易,因为您不必过多考虑编码(marshal)处理/序列化、引用与输出等。

此外,您无需告诉 .NET 它已经知道的内容。 float 数组是 R4 的 LPArray,您不必指定它。越简单越好。

所以,首先是flann_index_t。它在 C 中被定义为 void *,因此它显然必须是 IntPtr(指向“某物”的不透明指针)。

然后,结构。在 C 中作为简单指针传递的结构可以在 C# 中作为 ref 参数传递,如果您将其定义为 struct。如果您将其定义为 class,请不要使用 ref。一般来说,我更喜欢对 C 结构使用 struct。

您必须确保结构定义明确。通常,您使用 LayoutKind.Sequential,因为 .NET p/invoke 将以与 C 编译器相同的方式打包参数。所以你不必使用显式,特别是当参数是标准的(不是位的东西)比如 int,float,所以你可以删除所有 FieldOffset 并使用 LayoutKind.Sequential 如果所有成员都被正确声明......但这不是案例。

对于类型,就像我说的,你真的必须考虑二进制并问问自己你使用的每种类型,它的二进制布局是什么,大小? int 是(99.9% C 编译器)32 位。 float 和 double 是 IEEE 标准,所以应该永远不会有关于它们的问题。枚举通常基于 int,但这可能会有所不同(在 C 和 .NET 中,以便能够匹配 C)。 long 是(99.0% C 编译器)32 位,不是 64 位。所以 .NET 等效项是 Int32 (int),而不是 Int64 (long)。

因此您应该更正您的FlannParameters 结构并将long 替换为int。如果你真的想确保给定的结构,检查 Marshal.SizeOf(mystruct) 与 C 的 sizeof(mystruct) 使用与所使用的相同的 C 编译器编译你正在调用的库。他们应该是一样的。如果不是,则 .NET 定义(打包、成员大小、顺序等)中存在错误。

这里是经过修改的定义和似乎有效的调用代码。

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;
}

注意:我尝试使用特定的 flann 参数值,但在这种情况下库崩溃了,我不知道为什么......

关于c# - P/Invoke 在 C# 和非托管 DLL 之间编码和解码二维数组、结构和指针,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47007301/

相关文章:

c# - 程序崩溃时捕获异常并妥善处理

python - 检查列表是否不包含列表

python - 使用 range/arange 函数作为参数索引/切片 2d numpy 数组

c++ - linux C++,Qt中system()的参数

c++ - 按字典顺序比较字符串

c# - 在 C# 中,如何根据在 gridview 行中单击的按钮引用特定产品记录

c# - 有效且高效地管理许多 TCP 连接

c# - 带有 BeginReceive\EndReceive 的异步套接字意外数据包

python - 服务不可用 - urllib 代理不工作

c++ - Visual Studio 2013 中的推导失败,第一个模板参数是特定类型(这又是任意模板化的)