Mex文件:如何返回已分配的Matlab数组

5

我发现了一个非常棘手的问题,似乎很难轻松解决。简单来说,我想从一个mex文件中返回一个已经作为mex函数输入传递的数组。你可以轻松地这样做:

void mexFunction(int nargout, mxArray *pargout [ ], int nargin, const mxArray *pargin[])
{
   pargout[0] = pargin[0];
}

但这不是我需要的。我想从 pargin[0] 中获取原始指针,在内部进行处理,通过设置相应的数据指针来返回一个新创建的 mex 数组。就像这样:
#include <mex.h>

void mexFunction(int nargout, mxArray *pargout [ ], int nargin, const mxArray *pargin[])
{
  mxArray *outp;
  double *data;
  int m, n;

  /* get input array */
  data = mxGetData(pargin[0]);
  m = mxGetM(pargin[0]);
  n = mxGetN(pargin[0]);

  /* copy pointer to output array */
  outp = mxCreateNumericMatrix(0,0,mxDOUBLE_CLASS,mxREAL);
  mxSetM(outp, m);
  mxSetN(outp, n);
  mxSetData(outp, data);
  /* segfaults with or without the below line */
  mexMakeMemoryPersistent(data);
  pargout[0] = outp;
}

它不起作用。如果不是立即出现,那么经过几次调用后我会得到一个段错误。我相信在文档没有提到这种情况。唯一的要求是data指针必须使用mxCalloc分配,显然已经这样做了。因此,我认为这段代码是合法的。
我需要这样做,因为我正在将一个复杂的MATLAB结构解析成我的内部C数据结构。我处理数据,有些数据被重新分配,有些则没有。我想透明地返回输出结构,而不用考虑何时只需复制mxArray(第一个代码片段),何时实际上必须创建它。
请帮忙!
编辑
经过进一步查看和与Amro讨论,似乎甚至我的第一个代码片段也不受支持,并且在某些情况下可能导致MATLAB崩溃,例如将结构字段或单元素素传递给这样的mex函数:
>> a.field = [1 2 3];
>> b = pargin_to_pargout(a.field);   % ok - works and assigns [1 2 3] to b
>> pargin_to_pargout(a.field);       % bad - segfault

看来我将不得不走“未记录的MATLAB”道路,使用mxCreateSharedDataCopymxUnshareArray


为什么 pargout[0] = pargin[0]; 没有完成你想要的操作?如果这看起来不太靠谱,那么 mxCreateSharedDataCopy 是否能通过共享相同的数据指针并处理引用计数,从而防止 MATLAB 崩溃,达到你的需求呢? - chappjc
一个有趣的方法是使用共享数据。你能否将详细的解决方案发布为新答案? - Shai
1
@Shai:James Tursa提交的typecastx示例是如何实现这一点的绝佳范例;它基本上调用了plhs[0] = mxCreateSharedDataCopy(prhs[0]);而不是我在答案中写的mxDuplicateArray - Amro
@Shai 对于 mxCreateSharedDataCopy这里有一份详细但有点过时的例子,以及James Tursa撰写的快速示例。此外,还有这个不错的列表,列出了(半)未记录的MEX API函数。 - chappjc
我认为 pargin_to_pargout(a.field); 出现段错误是因为 nargout 为0,而 pargout[0] 是无效的。我在答案中添加了关于 mxCreateSharedDataCopy 的讨论。也许会有用。 - chappjc
显示剩余2条评论
2个回答

9

您应该使用mxDuplicateArray,这是文档化的方式:

#include "mex.h"

void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
{
    plhs[0] = mxDuplicateArray(prhs[0]);
}

1
我不想复制数组。我想创建一个新的数组,它保存与第一个数组相同的物理内存指针。mxDuplicateArray 将分配新的内存并复制数据。 - angainor
2
那样做可能很危险,会导致MATLAB崩溃。不过,有一些未记录的方法可以分享数据,可以参考这篇文章:https://dev59.com/J3bZa4cB1Zd3GeqPLe78#18849127。 - Amro
3
它崩溃的原因是两个mxArray结构指向同一块内存数据,但MATLAB并不知道这两个变量是“链接”的;所以当其中一个被释放时,另一个则处于不一致的状态,带有悬空指针... 在内部,MATLAB使用mxArray_tag结构中未记录的标志(类似引用计数)来跟踪这种情况。当您为常规MATLAB矩阵编写A = B;时,会看到此数据共享行为。有关更多信息,请参见该帖子评论中发布的链接。 - Amro
1
实际上,plhs [0] = prhs [0] 是合法的,尽管这种情况没有明确记录。输出数组是一个单独的 mxArray,它与输入数组共享其数据,同时在 MATLAB 侧更改/删除其中一个时被正确处理,不像 mxSetData 情况。我通常不建议这样做,除非你知道自己在做什么 :) 而且它有点令人困惑,因为它打破了输入 prhs 的常数性。请参阅此讨论:http://www.mathworks.com/matlabcentral/answers/77048 - Amro
1
有趣的是,我发现在某些情况下使用plhs[0] = prhs[0]会导致可靠的崩溃。我已经在http://www.mathworks.com/matlabcentral/answers/77048上回复了James Tursa的答案。所以即使这样做也是不正确的 :( 绝对没有(合法的)方法可以避免在那里来回复制内存。 - angainor
显示剩余5条评论

6
虽然未经文件记录,MEX API函数mxCreateSharedDataCopy曾被MathWorks公司提供作为创建mxArray共享数据副本的解决方案(由MathWorks提供),但现在似乎已被否认。MathWorks甚至在他们的解决方案中提供了一个示例{{link2:mxsharedcopy.c}}。
如同被删除的 MathWorks 解决方案(1-6NU359)中所描述的那样,该函数可用于克隆 mxArray 头文件。但是,使用 plhs[0] = prhs[0];plhs[0] = mxCreateSharedDataCopy(prhs[0]); 的区别在于,第一个版本只复制 mxArray*(指针),因此不会创建新的 mxArray 容器(至少直到 mexFunction 返回并 MATLAB 进行其操作为止),这将增加两个 mxArray 中数据的引用计数。
为什么这可能成为一个问题?如果您使用plhs[0] = prhs[0];,并且在从mexFunction返回之前没有对plhs[0]进行进一步修改,则一切都很好,由于MATLAB的帮助,您将拥有共享数据副本。然而,如果在上述赋值之后,您在MEX函数中修改了plhs[0],则由于prhs[0]引用相同的数据缓冲区,因此该更改也会在prhs[0]中看到。另一方面,当显式生成共享副本(使用mxCreateSharedDataCopy)时,存在两个不同的mxArray对象,并且对一个数组数据的更改将触发复制操作,从而产生两个完全独立的数组。此外,直接赋值有时会导致分段错误
以修改后的 mxsharedcopy.c 为例。该文件来自于上述MathWorks解决方案。第一个重要的步骤是提供mxCreateSharedDataCopy函数的原型。
/* Add this declaration because it does not exist in the "mex.h" header */
extern mxArray *mxCreateSharedDataCopy(const mxArray *pr);

正如注释所述,这不在 mex.h 中,因此您必须自己声明它。 mxsharedcopy.c 的下一部分以以下方式创建新的 mxArray
  1. A deep copy via mxDuplicateArray:

    copy1 = mxDuplicateArray(prhs[0]);
    
  2. A shared copy via mxCreateSharedDataCopy:

    copy2 = mxCreateSharedDataCopy(copy1);
    
  3. Direct copy of the mxArray*, added by me:

    copy0 = prhs[0]; // OK, but don't modify copy0 inside mexFunction!
    
然后它打印出每个 mxArray 的数据缓冲区(`pr`)的地址及其第一个值。以下是修改后的 `mxsharedcopy(x)` 对于 `x=ones(1e3);` 的输出:
prhs[0] = 72145590, mxGetPr = 18F90060, value = 1.000000
copy0   = 72145590, mxGetPr = 18F90060, value = 1.000000
copy1   = 721BF120, mxGetPr = 19740060, value = 1.000000
copy2   = 721BD4B0, mxGetPr = 19740060, value = 1.000000

发生了什么:
  1. 如预期所料,比较 prhs[0]copy0,我们没有创建任何新的东西,只是另一个指向同一 mxArray 的指针。
  2. 比较 prhs[0]copy1,注意到 mxDuplicateArray 在地址为 721BF120 处创建了一个新的 mxArray,并将数据复制到了地址为 19740060 的新缓冲区中。
  3. copy2copy1 有不同的地址(mxArray*),这意味着它们是不同的 mxArray,而不仅仅是由不同变量指向的同一对象,但它们在地址为 19740060 的相同数据。
这个问题可以简化为:在 plhs [0] 中返回简单指针复制或者 mxCreateSharedDataCopycopy0copy2 是否安全,还是必须使用实际复制数据的 mxDuplicateArray?我们可以通过销毁 copy1 并验证 copy2 仍然有效来证明 mxCreateSharedDataCopy 是可行的。
mxDestroyArray(copy1);
copy2val0 = *mxGetPr(copy2); % no crash!

将文本翻译成中文:

应用共享数据复制到输入

回到问题。比MathWorks的例子更进一步,返回一个输入的共享数据副本。只需执行:

if (nlhs>0) plhs[0] = mxCreateSharedDataCopy(prhs[0]);

屏住呼吸!

>> format debug
>> x=ones(1,2)
x =

Structure address = 9aff820     % mxArray*
m = 1
n = 2
pr = 2bcc8500                   % double*
pi = 0
     1     1
>> xDup = mxsharedcopy(x)
xDup =

Structure address = 9afe2b0     % mxArray* (different)
m = 1
n = 2
pr = 2bcc8500                   % double* (same)
pi = 0
     1     1
>> clear x
>> xDup % hold your breath!
xDup =

Structure address = 9afe2b0 
m = 1
n = 2
pr = 2bcc8500                    % double* (still same!)
pi = 0
     1     1

现在进行临时输入(不包括format debug):

>> tempDup = mxsharedcopy(2*ones(1e3));
>> tempDup(1)
ans =
     2

有趣的是,如果我不使用mxCreateSharedDataCopy进行测试(即只使用plhs[0] = prhs[0];),MATLAB不会崩溃,但输出变量永远不会出现。
>> tempDup = mxsharedcopy(2*ones(1e3)) % no semi-colon
>> whos tempDup
>> tempDup(1)
Undefined function 'tempDup' for input arguments of type 'double'.

R2013b,Windows,64位。
mxsharedcopy.cpp(修改的C ++版本):
#include "mex.h"

/* Add this declaration because it does not exist in the "mex.h" header */
extern "C" mxArray *mxCreateSharedDataCopy(const mxArray *pr);
bool mxUnshareArray(const mxArray *pr, const bool noDeepCopy); // true if not successful

void mexFunction(int nlhs,mxArray *plhs[],int nrhs,const mxArray *prhs[])
{
    mxArray *copy1(NULL), *copy2(NULL), *copy0(NULL);

    //(void) plhs; /* Unused parameter */

    /* Check for proper number of input and output arguments */
    if (nrhs != 1)
        mexErrMsgTxt("One input argument required.");
    if (nlhs > 1)
        mexErrMsgTxt("Too many output arguments.");

    copy0 = const_cast<mxArray*>(prhs[0]); // ADDED

    /* First make a regular deep copy of the input array */
    copy1 = mxDuplicateArray(prhs[0]);

    /* Then make a shared copy of the new array */
    copy2 = mxCreateSharedDataCopy(copy1);

    /* Print some information about the arrays */
    //     mexPrintf("Created shared data copy, and regular deep copy\n");
    mexPrintf("prhs[0] = %X, mxGetPr = %X, value = %lf\n",prhs[0],mxGetPr(prhs[0]),*mxGetPr(prhs[0]));
    mexPrintf("copy0   = %X, mxGetPr = %X, value = %lf\n",copy0,mxGetPr(copy0),*mxGetPr(copy0));
    mexPrintf("copy1   = %X, mxGetPr = %X, value = %lf\n",copy1,mxGetPr(copy1),*mxGetPr(copy1));
    mexPrintf("copy2   = %X, mxGetPr = %X, value = %lf\n",copy2,mxGetPr(copy2),*mxGetPr(copy2));

    /* TEST: Destroy the first copy */
    //mxDestroyArray(copy1);
    //copy1 = NULL;
    //mexPrintf("\nFreed copy1\n");
    /* RESULT: copy2 will still be valid */
    //mexPrintf("copy2 = %X, mxGetPr = %X, value = %lf\n",copy2,mxGetPr(copy2),*mxGetPr(copy2));

    if (nlhs>0) plhs[0] = mxCreateSharedDataCopy(prhs[0]);
    //if (nlhs>0) plhs[0] = const_cast<mxArray*>(prhs[0]);
}

嗨,chappjc,你的回答很好 - 但是我有个不好的消息:我刚刚尝试了一下Matlab R2014A Pre,并发现这个很好的未记录的mxCreateSharedDataCopy和其他一些东西都消失了。太遗憾了。 - Bastian Ebeling
2
@BastianEbeling 我注意到在R2014a中,mxCreateSharedDataCopy现在作为一个带修饰的C++函数导出(名称重整是与编译器相关的);没有修饰的名称是 struct mxArray_tag * matrix::detail::noninlined::mx_array_api::mxCreateSharedDataCopy(struct mxArray_tag const* )。看起来MathWorks已经对代码进行了一些重构,将MX API函数组织到不同的命名空间中。无论如何,这对我仍然有效,但请注意,mex现在具有不同的C和C ++配置。你是否遇到链接器错误? - chappjc
新年快乐,chappjc!我发现C++也有一个混淆的问题 - 但对我来说,在普通的c-mex中没有运行该函数的机会。是的:导致链接器错误。我不想将我的c-mex代码重写为c++-mex:你知道我是什么意思吗? - Bastian Ebeling
也许提供更多细节会更清晰:在Windows环境下编译的结果是:使用'Microsoft Windows SDK 7.1(C)'进行构建。 使用mex时出错 创建库double2byte.lib和对象double2byte.exp double2byte.obj: error LNK2019: 未解析的外部符号mxCreateSharedDataCopy,在函数mexFunction中引用 double2byte.mexw64: 致命错误LNK1120: 1个未解决的外部 - Bastian Ebeling
对于有兴趣的人,R2014a的解决方案可以在此处找到(链接) ,由Bastian Ebeling提供。 - Yair Altman
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接