Matlab与VC混合编程之二
1. 待解决的新问题
在《Matlab与VC混合编程之一》中我们已经实现了在VC中调用Matlab编译M文件生成的COM的方法,并成功的进行了计算并绘制一个点。接下来的问题是如何向COM接口函数传递一个向量或者一个矩阵。为什么需要学习向COM接口函数传递一个向量或者一个矩阵?很多学科都涉及向量和矩阵的相关计算,尤其是矩阵,经常需要求取矩阵的特征值、特征向量、行列式、逆矩阵、LU分解等。并且向量和矩阵也是Matlab计算的基本单元。现在我们就来研究如何向COM接口函数传递向量或者矩阵,如何获取COM接口函数计算的结果。
2. 编写相关的M文件并生成COM
为了演示如何显现标量、向量和矩阵如何在VC和COM中传递,我们首先需要编写使用这些类型作为输入输出变量的M文件。
a. 输入输出变量都是向量
function y = myCross(a, b)
%实现向量叉乘,返回向量
y = cross(a, b);
b. 输入是矩阵,输出是标量
function y = myDet(X)
%实现求矩阵行列式,返回标量
y = det(X);
c. 输入是矩阵,输出是矩阵
function Y = myInv(X)
%实现求矩阵的逆矩阵,返回矩阵
Y = inv(X);
现在,按照《Matlab与VC混合编程之一》“4.将Matlab函数变成COM”的方法,将这3个M文件编译成COM。COM组件名字是MyCalc,将类集合中给出的默认类名改为MyCalc,具体是将默认的类名删掉,添加类名MyCalc。这一步的工作已经完成。接下来是将COM文件导入到VC工程中。
3. 建立VC工程导入COM文件
我们新建一个基于对话框的MFC工程TestMyCalc作为示例。按照《Matlab与VC混合编程之一》“5.在VC中使用COM”中前5步(a、b、c、d、e)的步骤对TestMyCalc工程添加Button,COM相关文件,编写代码等工作。不同的是步骤b中添加的文件是mwcomtypes.h,mycalc_idl.h,mycalc_idl_i.c,步骤c在相应的mycalc_idl_i.c中查找,步骤d添加相应的mycalc_idl.h头文件。OnButtonTest()中函数流程即四个步骤完全相同,个别的命名有所改变。详细代码见附录的a部分。
4. 了解VARIANT
在VC中,我们一般使用一个double型变量表示标量,一维double数组表示向量,二维double数组表示矩阵。那么怎么把这些变量写入VARIANT型变量并放入COM接口函数,又怎么从VARIANT型变量中读出计算的结果?要回答这两个问题,首先要了解VARIANT类型。
《Matlab与VC混合编程之一》中已经说了,COM是不依赖于特定的开发语言。VAIRANT是COM使用的类型,当然也应该不依赖于特定的开发语言。不同开发语言有不同的变量类型,同一种变量类型的类型关键字也可能不同。为了进行统一描述,VARIANT类型包含了两部分,一部分是VARIANT包含的数据的类型,另一部分是该数据的类型。可以把VARIANT看成是一个包装盒,封装了语言相关的变量的值和变量的类型。
比如我们在VC中定义一个VARIANT变量,VARIANT a; 那么a.vt的部分用来描述变量类型(value_type)。如果想让a封装double型变量b,那么赋值a.vt = VT_R8;其中VT_R8表示double,因为double型数据是实数(Real),一个double型变量占用8个字节(即sizeof(double)的结果是8)。之后将给数据部分赋值a.dblval=b;其中dblval表示double value。
比如我们在VC中定义另一个VARIANT变量,VARIANT b;如果想让b封装double型数组,那么赋值b.vt = VT_R8|VT_ARRAY; 这里的数组没有指出维数,可以是任意维的,它真正的维数是靠b的数据部分来描述。给b的数据部分b.parray赋值比较麻烦。这里数据部分是一个指向数组的指针(pointer to array)。查看b.parray的类型,其类型为(SAFEARRAY *),指向SAFEARRAY的指针。SAFEARRAY顾名思义,是一个安全数组的类型。通过查看MSDN,可知其详细信息,不过我们不用知道的这么详细,为了方便使用SAFEARRAY,VC特意定义了一些函数供我们使用。常用的有:SafeArrayCreate创建之,SafeArrayAccessData得到数据的指针和访问权限,成块的存取包含的数据,SafeArrayUnaccessData不再有权限访问数据,SafeArrayDestroy销毁之。与成块的访问数据对应的还有一种单个元素的访问方式,SafeArrayPutElement单个的放入元素,SafeArrayGetElement单个的读入元素。这种访问方式没有成块的访问方式效率高。我这里使用的成块的访问方式。想深入了解SafeArray及其访问方式的可以参考http://blog.csdn.net/sheismylife/article/details/234547 《SAFEARRAY使用实例》该博文对这些方面给予了深入的介绍。下面给出创建、写入、读出包装了一维、二维数组的VARIANT变量方法。
5. 一维数组的使用
a. 创建
方式一,更通用
VARIANT b;
b.vt = VT_R8|VT_ARRAY;
SAFEARRAYBOUND sfbound[1]; //一维数组时使用
sfbound[0].lLbound = 0; //该维数组的起始下标0
sfbound[0].cElements = 3; //该维数组的大小3
b.parray = SafeArrayCreate(VT_R8, 1, sfbound);//double型,一维,维数描述
方式二,更简单
VARIANT b;
b.vt = VT_R8|VT_ARRAY;
b.parray = SafeArrayCreateVector(VT_R8, 0, 3););//一维double型,起始下标0,大小3。
b. 写入数据
double array1[3] = {1, 2, 3}; //待存入的一维数组
double* buf = NULL;
SafeArrayAccessData(b.parray, (void**)&buf); //指针指向SafeArray的数据区
memcpy(buf, array1, sizeof(double)*3); //将array1的3个double数据向指针指向的数据区拷贝
SafeArrayUnaccessData(b.parray); //解除指针的指向
c. 读出数据,与写入数据的方式类似,只是数据拷贝的方向相反
double array2[3] = {0};
double* buf2 = NULL;
SafeArrayAccessData(b.parray, (void**)&buf2); //指针指向SafeArray的数据区
memcpy(array2, buf2, sizeof(double)*3); //将指针指向的数据区的3个double数据向array2拷贝
SafeArrayUnaccessData(b.parray); //解除指针的指向
上述的3个过程实现了将一维数组(向量){1,2,3}从array1放入VARIANT并读到array2的过程。
6. 二维数组的使用
a. 创建
VARIANT b;
b.vt = VT_R8|VT_ARRAY;
SAFEARRAYBOUND sfbound[2]; //二维数组时使用
sfbound[0].lLbound = 0; //该维数组的起始下标0
sfbound[0].cElements = 3; //该维数组的大小3
sfbound[1].lLbound = 0; //该维数组的起始下标0
sfbound[1].cElements = 2; //该维数组的大小2
b.parray = SafeArrayCreate(VT_R8, 2, sfbound);//double型,二维,维数描述
b. 写入数据
这里尤其要注意:VC二维数组的内存布局是行优先的,而SAFEARRAY是列优先的。
举例如下:一个3行2列的数组double array1[3][2] = {1, 2, 3, 4, 5, 6};在VC中是先存第1行{1,2},再存第2行{3,4},再存第3行{5,6}。写成矩阵的形式是:
[1 2;
3 4;
5 6]
而SAFEARRAY的存放是这样的,先存第1列{1,2,3},再存第2列{4,5,6}。写成矩阵的形式是
[1 4;
2 5;
3 6]。
显然两者是不等价的。直接把这样的数据交给Matlab生成的COM组件去执行肯定达不到正确的结果。所以在对SAFEARRAY读写时需要下标变换。变换的规则是将double数组和SAFEARRAY都看做一维数组,double数组按行优先的顺序访问,行优先的规则是[i][j] = [i*nCols +j];SAFEARRAY数组按照列优先的顺序访问,列优先的规划是[i][j] = [i + j*nRows]。其中数组的大小是nRows行,nCols列。
考察上面的例子,存入SAFEARRAY时,令buf[i + j*3] = ptarray1[i*2 + j]。其中ptarray1是指向二维数组的指针,double* ptarray1 = (double*)array1; buf是指向SAFEARRAY数据区的指针。当i从0到2,j从0到1遍历时,ptarray1的数据在内存中的排序是{1,2,3,4,5,6},经过变化后放入buf,buf的数据在内存中的排序是{1,3,5,2,4,6}。存入SAFEARRAY时按照列优先来写数据,故第1列是{1,3,5},第2列是{2,4,6},写成矩阵的形式是:
[1 2;
3 4;
5 6]。这个数据正好是需要的矩阵,交给Matlab生成的COM组件去执行能够得到正确的结果。下面给出实现代码。
double array1[3][2] = {1, 2, 3, 4, 5, 6};
double* ptarray1 = (double*)array1;
double* buf = NULL;
SafeArrayAccessData(b.parray, (void**)&buf); //指针指向SafeArray的数据区
//数据变换
for (int j = 0; j < 2; ++j) //ptrray1的第j列
{
for (int i = 0; i < 3; ++i)//ptarray1的第i行
{
buf[i + j*3] = ptarray1[i*2 + j]; //行优先转换为列优先
}
}
SafeArrayUnaccessData(b.parray); //解除指针的指向
iii.读出数据,与写入数据的方式类似,只是数据拷贝的方向相反
double array2[3][2] = {0};
double* ptarray2 = (double*)array2;
double* buf2 = NULL;
SafeArrayAccessData(b.parray, (void**)&buf2); //指针指向SafeArray的数据区
//数据变换
for (j = 0; j < 2; ++j) //ptrray1的第j列
{
for (int i = 0; i < 3; ++i)//ptarray1的第i行
{
ptarray2[i*2 + j] = buf[i + j*3]; //列优先转换为行优先
}
}
SafeArrayUnaccessData(b.parray); //解除指针的指向
上述的3个过程实现了将二维数组(矩阵)[1,2;
3,4;
5,6]从array1放入VARIANT并读到array2的过程。
7. 回答Action_1到Action_6的问题
经过了对第4、5、6节的学习,我们就能够回答Action_1到到Action_6的问题了。详细的回答见附录b部分。
8. 有没有可以改进的空间?
标量、向量的方式还可以接受,对矩阵的处理简直令人发狂,难道每次面临矩阵的运算和处理都要使用这么麻烦的方式处理,有没有可以改进的访问方式?答案是肯定的,我们可以用C++的类来封装这种复杂性,以后再使用时就直接调用类的接口,可以很简单很优雅的使用Matlab生产的COM组件了。具体的实现方式,将在第三部分继续分解。
9. 附录
a. OnButtonTest()的源码,其中Action_1到Action_6需要填充。
void CTestMyCalcDlg::OnButtonTest()
{
// TODO: Add your control notification handler code here
::CoInitialize(NULL); //a.初始化COM。
IMyCalc *pICalc = NULL; //声明一个接口
//b.创建COM接口。
HRESULT hr = ::CoCreateInstance(CLSID_MyCalc, //你使用的COM对象的ID(CLaSs ID)
NULL,
CLSCTX_ALL,
IID_IMyCalc, //你使用的COMCOM对象接口的ID(Interface ID)
(LPVOID*)&pICalc);//这里用&pIdraw使pIdraw指向接口的地址
if (FAILED(hr)) //错误检查并处理
{
//。。。
return;
}
//c.使用COM接口,这一段使用的数据类型是VARIANT
long nvar = 1; //输出变量的个数
//i. 使用mycross
VARIANT ca,cb,cy; //ca,cb输入参数,cy输出参数
//对x,y初始化
VariantInit(&ca);
VariantInit(&cb);
VariantInit(&cy);
//对向量ca,cb赋值,以double型为例
//...(怎么做?Action_1
hr = pICalc->mycross(nvar, &cy, ca, cb); //输出变量使用&cy形式才能得到返回值。
//从cy获取计算结果
//...(怎么做?Action_2
//ii. 使用mydet
VARIANT dx,dy; //dx输入参数,dy输出参数
//对x,y初始化
VariantInit(&dx);
VariantInit(&dy);
//对向量dx赋值,以double型为例
//...(怎么做?Action_3
hr = pICalc->mydet(nvar, &dy, dx); //输出变量使用&dy形式才能得到返回值。
//从dy获取计算结果
//...(怎么做?Action_4
//iii. 使用myinv
VARIANT ix,iy; //ix输入参数,iy输出参数
//对x,y初始化
VariantInit(&ix);
VariantInit(&iy);
//对向量ix赋值,以double型为例
//...(怎么做?Action_5
hr = pICalc->myinv(nvar, &iy, ix); //输出变量使用&iy形式才能得到返回值。
//从iy获取计算结果
//...(怎么做?Action_6
//d.退出COM。
CoUninitialize();
}
b.OnButtonTest()的源码,其中Action_1到Action_6已经用正确的代码填充。
void CTestMyCalcDlg::OnButtonTest()
{
// TODO: Add your control notification handler code here
::CoInitialize(NULL); //a.初始化COM。
IMyCalc *pICalc; //声明一个接口
//b.创建COM接口。
HRESULT hr = ::CoCreateInstance(CLSID_MyCalc, //你使用的COM对象的ID(CLaSs ID)
NULL,
CLSCTX_ALL,
IID_IMyCalc, //你使用的COMCOM对象接口的ID(Interface ID)
(LPVOID*)&pICalc);//这里用&pIdraw使pIdraw指向接口的地址
if (FAILED(hr)) //错误检查并处理
{
//。。。
return;
}
//c.使用COM接口,这一段使用的数据类型是VARIANT
long nvar = 1; //输出变量的个数
//i. 使用mycross
VARIANT ca,cb,cy; //ca,cb输入参数,cy输出参数
//对x,y初始化
VariantInit(&ca);
VariantInit(&cb);
VariantInit(&cy);
//对向量ca,cb赋值,以double型为例
//...Action_1
ca.vt = VT_R8|VT_ARRAY;
cb.vt = VT_R8|VT_ARRAY;
double array1[3] = {1, 0, 0}; //待存入的一维数组
double array2[3] = {0, 1, 0}; //带存入的一维数组
double* buf = NULL;
ca.parray = SafeArrayCreateVector(VT_R8, 0, 3);
cb.parray = SafeArrayCreateVector(VT_R8, 0, 3);
SafeArrayAccessData(ca.parray, (void**)&buf); //指针指向SafeArray的数据区
memcpy(buf, array1, sizeof(double)*3); //将array1的3个double数据向指针指向的数据区复制
SafeArrayUnaccessData(ca.parray); //解除指针的指向
SafeArrayAccessData(cb.parray, (void**)&buf); //指针指向SafeArray的数据区
memcpy(buf, array2, sizeof(double)*3); //将array1的3个double数据向指针指向的数据区复制
SafeArrayUnaccessData(cb.parray); //解除指针的指向
hr = pICalc->mycross(nvar, &cy, ca, cb); //输出变量使用&cy形式才能得到返回值。
//从cy获取计算结果
//...Action_2
double array3[3] = {0};
SafeArrayAccessData(cy.parray, (void**)&buf); //指针指向SafeArray的数据区
memcpy(array3, buf, sizeof(double)*3); //将array1的3个double数据向指针指向的数据区复制
SafeArrayUnaccessData(cy.parray); //解除指针的指向
//array3结果是{0,0,1},结果正确。
//ii. 使用mydet
VARIANT dx,dy; //dx输入参数,dy输出参数
//对x,y初始化
VariantInit(&dx);
VariantInit(&dy);
//对向量dx赋值,以double型为例
//...Action_3
double array4[2][2] = {1, 0, 0, 4};
double* ptarray4 = (double*)array4;
SAFEARRAYBOUND sfbound[2]; //二维数组时使用
sfbound[0].lLbound = 0; //该维数组的起始下标0
sfbound[0].cElements = 2; //该维数组的大小2
sfbound[1].lLbound = 0; //该维数组的起始下标0
sfbound[1].cElements = 2; //该维数组的大小2
dx.vt = VT_R8|VT_ARRAY;
dx.parray = SafeArrayCreate(VT_R8, 2, sfbound); //double型,二维,维数描述
buf = NULL;
SafeArrayAccessData(dx.parray, (void**)&buf); //指针指向SafeArray的数据区
//数据变换
for (int j = 0; j < 2; ++j) //ptrray4的第j列
{
for (int i = 0; i < 2; ++i)//ptarray4的第i行
{
buf[i + j*2] = ptarray4[i*2 + j]; //行优先转换为列优先
}
}
SafeArrayUnaccessData(dx.parray); //解除指针的指向
hr = pICalc->mydet(nvar, &dy, dx); //输出变量使用&dy形式才能得到返回值。
//从dy获取计算结果
//...Action_4
double dresult = 0;
if (dy.vt == VT_R8)
{
dresult = dy.dblVal;
}
//dresult = 4,结果正确
//iii. 使用myinv
VARIANT ix,iy; //ix输入参数,iy输出参数
//对x,y初始化
VariantInit(&ix);
VariantInit(&iy);
//对向量ix赋值,以double型为例
//Action_5
double array5[3][3] = {1, 0, 0, 0, 1, 0, 0, 0, 1};
double* ptarray5 = (double*)array5;
SAFEARRAYBOUND sfbound2[2]; //二维数组时使用
sfbound2[0].lLbound = 0; //该维数组的起始下标0
sfbound2[0].cElements = 3; //该维数组的大小2
sfbound2[1].lLbound = 0; //该维数组的起始下标0
sfbound2[1].cElements = 3; //该维数组的大小2
ix.vt = VT_R8|VT_ARRAY;
ix.parray = SafeArrayCreate(VT_R8, 2, sfbound2); //double型,二维,维数描述
buf = NULL;
SafeArrayAccessData(ix.parray, (void**)&buf); //指针指向SafeArray的数据区
//数据变换
for (j = 0; j < 3; ++j) //ptrray4的第j列
{
for (int i = 0; i < 3; ++i)//ptarray4的第i行
{
buf[i + j*3] = ptarray5[i*3 + j]; //行优先转换为列优先
}
}
SafeArrayUnaccessData(ix.parray); //解除指针的指向
hr = pICalc->myinv(nvar, &iy, ix); //输出变量使用&iy形式才能得到返回值。
//从iy获取计算结果
//...Action_6
double array6[3][3] = {0};
double* ptarray6 = (double*)array6;
buf = NULL;
SafeArrayAccessData(iy.parray, (void**)&buf); //指针指向SafeArray的数据区
//数据变换
for (j = 0; j < 3; ++j) //ptrray4的第j列
{
for (int i = 0; i < 3; ++i)//ptarray4的第i行
{
ptarray6[i*3 + j] = buf[i + j*3]; //列优先转换为行优先
}
}
SafeArrayUnaccessData(iy.parray); //解除指针的指向
//array6结果是{{1,0,0}, {0,1,0}, {0,0,1}},结果正确。
//d.退出COM。
CoUninitialize();
}
本文是作者原创,转载必须保证文章的完整性并标明出处(blog.sina.com.cn/zhangkunhn),请尊重作者,支持原创。