Matlab: How to modify a struct or cell array in a mex-file without using "mxDuplicateArray"
The internal structure of how Matlab stores a variable as well as the concept of sharing variables using crosslinks are described in an article from Peter Boetcher.
For creating shared copies of an mxArray, the functions "mxCreateSharedDataCopy", "mxUnshareArray", and "mxUnreference" are used. Since not documented, the following might not be correct. But it has been tested and seems to work for Matlab versions R14, 2006b, 2007a. Any ideas about how doing it better are welcome.
Examine the disassembly, I guess that the three functions have the following syntax:mxArray *mxCreateSharedDataCopy(const mxArray *pr);
bool mxUnshareArray(const mxArray *pr, const bool noDeepCopy); // true if not successful
mxArray *mxUnreference(const mxArray *pr);
They are described in the following. The const declaration is not fully correct since the array headers change. But with it, the compiler does not complain about different const declarations when assigning right-hand-side arguments.
Take a variable x created in Matlab:
x = {[1 2], [3 4]};
In a C-mex-file, this cell array looks as follows. "x" points to an mxArray whose data pointer "pdata" points to an array of two pointers, each pointing to an mxArray (an mxArray is indicated by a rectangle):
mxArray *x;
*x:
+==================+
| crosslink = NULL |
| refcount = 0 |
| pdata ----------+------> [mxArray* mxArray*]
+==================+ | |
\ / \ /
+==================+ +==================+
| crosslink = NULL | | crosslink = NULL |
| refcount = 0 | | refcount = 0 |
| pdata -+ | | pdata -+ |
+=========|========+ +=========|========+
| |
\ / \ /
[1 2] [3 4]
If we want to change the content of the first cell, we first must create a copy of x. The following procedere is equivalent for structure arrays, except the mxGetCell and mxSetCell functions must be replaced by the accordant functions for structure arrays.
mxArray *y = mxCreateSharedDataCopy(x);
The data pointer of y points to the same array as x does. A crosslink is set between x and y. Hence, only the array header is copied, the actual data is not:
*x:
+==================+
| crosslink = y |
| refcount = 0 |
| pdata ----------+------> [mxArray* mxArray*]
+==================+ / \ | |
| \ / \ /
| +==================+ +==================+
| | crosslink = NULL | | crosslink = NULL |
| | refcount = 0 | | refcount = 0 |
| | pdata -+ | | pdata -+ |
| +=========|========+ +=========|========+
| | |
| \ / \ /
| [1 2] [3 4]
*y: |
+==================+ |
| crosslink = x | |
| refcount = 0 | |
| pdata ----------+---+
+==================+
To be able to modify a cell of y, we must unshare both arrays:
mxUnshareArray(y, true); // true: do not make a deep copy
The array of mxArray-pointers is copied and the crosslink between x and y is removed. The reference counter (refcount) of each mxArray in the cell is increased by one. The data is still shared.
*x:
+==================+
| crosslink = NULL |
| refcount = 0 |
| pdata ----------+------> [mxArray* mxArray*]
+==================+ | |
\ / \ /
+==================+ +==================+
*first_cell:| crosslink = NULL | | crosslink = NULL |
| refcount = 1 | | refcount = 1 |
| pdata -+ | | pdata -+ |
+=========|========+ +=========|========+
/ \ | / \ |
| \ / | \ /
| [1 2] | [3 4]
*y: | |
+==================+ | |
| crosslink = NULL | | |
| refcount = 0 | | |
| pdata ----------+------> [mxArray* mxArray*]
+==================+
Now we want to change the first cell of y. The shared mxArray "first_cell" has to be copied using the function "mxUnreference". Get the first mxArray:
mxArray *first_cell = mxGetCell(y, 0);
Note that the following yields the same result, since the mxArray only exists once (y is replaced by x):
mxArray *first_cell = mxGetCell(x, 0);
The mxUnreference function copies the mxArray and creates a crosslink between the copies. The affected reference counter is decreased by 1:
mxArray *first_cell_copy = mxUnreference(first_cell);
*x:
+==================+
| crosslink = NULL |
| refcount = 0 |
| pdata ----------+------> [mxArray* mxArray*]
+==================+ | |
\ / \ /
+==================+ +==================+
*first_cell:| crosslink = f_c_c| | crosslink = NULL |
| refcount = 0 | | refcount = 1 |
| pdata -+ | | pdata -+ |
+=========|========+ +=========|========+
| <-----+ / \ |
\ / | | \ /
[1 2] | | [3 4]
*y: | |
+==================+ | |
| crosslink = NULL | | |
| refcount = 0 | | |
| pdata ----------+------> [mxArray* | mxArray*]
+==================+ | |
\ / |
+=================|+
*first_cell_copy:| crosslink = f_c ||
| refcount = 0 ||
| pdata ---------+|
+==================+
To assign a new value to y{1}, the crosslink between first_cell and first_cell_copied must be removed. This can be done by either destroying the mxArray "first_cell_copy" and assigning a new one to y{1}:
mxDestroyArray(first_cell_copy);
mxSetCell(y, 0, new_mxArray);
or by unsharing it and assigning the new data pointer:
mxUnshareArray(first_cell_copy);
mxSetData ...
If first_cell is another cell or structure array, one can recursively repeat all the steps above (after mxCreateShareCopy). It is noteworthy that all this can also be applied on objects, since objects in Matlab are stored as structure arrays.
The steps above are equivalent to the following Matlab-Code:
y = x;
y{1} = ...
In a first version I used the function "mxCreateSharedCopy" instead of "mxCreateSharedDataCopy". In most cases, these functions behave equally. They do not if the mxArray to be copied is marked as being a temporal array. This is the case if the result of an expression, and not a variable, is used as the argument of a function. For instance, in the statement
y = foo(x{1}, x);
the first argument given to the function "foo" is a temporal mxArray, while the second one is not. In such a case, calling p2 = mxCreateSharedCopy(p1) (where p1 is a pointer to the mxArray x{1}) does not actually create a shared copy, it rather returns the given pointer to the mxArray. That is, p2 is equal to p1. This can cause problems because the temporal array gets destroyed when the function is finished; hence, the pointer to this array is not valid anymore. Using the function "mxCreateSharedDataCopy" instead works as intended. It really creates a shared copy and the mxArray does not get destroyed.
Suggestions for improvements are welcome. I hope the infos are useful to someone. In my case, it gives a great performance benefit when modifying a certain property of a large number of objects. Below is a program demonstrating the use of the three functions.
/************************************************************************
* syntax in Matlab:
* function var_struct_copied = foo(var_struct, value, duplicate_array)
*
* Returns the structure variable "var_struct" with the first field set to
* "value". If "duplicate_array" is false or omitted, a shared copy is
* created. Otherwise a deep copy of the struct array is returned
* (using "mxDuplicateArray").
*************************************************************************/
#include "mex.h"
// declare undocumented functions
extern mxArray *mxCreateSharedDataCopy(const mxArray *pr);
extern bool mxUnshareArray(const mxArray *pr, const bool noDeepCopy);
extern mxArray *mxUnreference(const mxArray *pr);
/* Definition of structure mxArray_tag for debugging purposes. Might not
* be fully correct for Matlab 2006b or 2007a, but the important things
* are. Thanks to Peter Boettcher.
*/
struct mxArray_tag {
const char *name;
mxClassID class_id;
int vartype;
mxArray *crosslink;
int number_of_dims;
int refcount;
struct {
unsigned int scalar_flag : 1;
unsigned int flag1 : 1;
unsigned int flag2 : 1;
unsigned int flag3 : 1;
unsigned int flag4 : 1;
unsigned int flag5 : 1;
unsigned int flag6 : 1;
unsigned int flag7 : 1;
unsigned int private_data_flag : 1;
unsigned int flag8 : 1;
unsigned int flag9 : 1;
unsigned int flag10 : 1;
unsigned int flag11 : 4;
unsigned int flag12 : 8;
unsigned int flag13 : 8;
} flags;
int rowdim;
int coldim;
union {
struct {
double *pdata; // original: void*
double *pimag_data; // original: void*
void *irptr;
void *jcptr;
int nelements;
int nfields;
} number_array;
struct {
mxArray **pdata;
char *field_names;
void *dummy1;
void *dummy2;
int dummy3;
int nfields;
} struct_array;
struct {
void *pdata; /*mxGetInfo*/
char *field_names;
char *name;
int checksum;
int nelements;
int reserved;
} object_array;
} data;
};
// Matlab gatway function
void mexFunction(int nlhs, mxArray *plhs[],
int nrhs, const mxArray *prhs[])
{
const mxArray *p_in,
*mx_value; // value to be assigned
mxArray *p_return; // mxArray to be returned
bool duplicate_array = false; // duplicate or shallow?
if (nrhs < 2) {
mexErrMsgTxt("Two input arguments are expected.");
} else {
p_in = prhs[0];
mx_value = prhs[1];
if (!mxIsStruct(p_in))
mexErrMsgTxt("First argument must be a structure array.");
}
if (nrhs == 3) {
const mxArray *mx_arg = prhs[2];
if (mxIsLogicalScalar(mx_arg)) {
duplicate_array = *mxGetLogicals(mx_arg);
} else
mexErrMsgTxt("Third argument must be a logical scalar.");
}
if (duplicate_array) {
// duplicate given arrays, set value and return
p_return = mxDuplicateArray(p_in);
mxSetFieldByNumber(p_return, 0, 0, mxDuplicateArray(mx_value));
} else {
mxArray *mx_field, *mx_field_shared;
// create shared copy
p_return = mxCreateSharedDataCopy(p_in);
// unshare array
mxUnshareArray(p_return, true);
// unreference first field
mx_field = mxGetFieldByNumber(p_return, 0, 0);
mx_field_shared = mxUnreference(mx_field);
// mxArray *mx_field has been copied and is shared (crosslinked) with *mx_field_shared
// destroy shared array (removes crosslink), since we want to set a new one
mxDestroyArray(mx_field_shared);
// set new array
mxSetFieldByNumber(p_return, 0, 0, mxCreateSharedDataCopy(mx_value));
}
plhs[0] = p_return;
}
Contributors : Benjamin Schubert
Last modified 09.12.2007 23:47