Mathematica和C/C++:数据交换

7
我想知道如何在Mathematica和C/C++之间使用管道交换数据。在Mathematicatutorial中,它说“当您打开文件或管道时,Mathematica会创建一个“流对象”,指定与文件或管道关联的打开流”。
我知道如何在C和Mathematica中创建文件,并使每个程序读取和写入它们。但我仍然不知道如何通过管道从C发送输出到另一个程序,更不用说如何从Mathematica中做到这一点了。
下面是一个函数,在其中Mathematica将矩阵写入二进制文件,并读取以该格式编写的文件。
writeDoubleMatrix[obj_, fileName_] := Module[{file},
  file = OpenWrite[fileName, BinaryFormat -> True];
  BinaryWrite[file, Length@obj, "Integer32"];
  BinaryWrite[file, Length@obj[[1]], "Integer32"];
  BinaryWrite[file, Flatten[obj], "Real64"];
  Close[file]
 ]
readDoubleMatrix[fileName_] := Module[{file, obj, m, n},
  file = OpenRead[fileName, BinaryFormat -> True];
  m = BinaryRead[file, "Integer32"];
  n = BinaryRead[file, "Integer32"];
  obj = BinaryReadList[file, "Real64", m*n];
  Close[file];
  Partition[obj, n]
 ]

第一个函数将把2个整数(矩阵的大小)和矩阵的数据写入文件。我没有进行任何错误检查,因此假设要写入的数据特定形式为{{r11, r12, ..., r1n}, ...., {rm1, rm2, ..., rmn}}。第二个函数可以读取二进制文件并返回矩阵。
接下来是我的C程序。该程序将读取存储在文件MathematicaData.bin中的数据,将此矩阵乘以2并将数据写入另一个文件。
// genData.c
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char** argv){
    int m, n, i;
    double* matrix;
    FILE* fin;
    FILE* fout;

    // Reading input file
    fin = fopen(argv[1], "rb");
    fread(&m, sizeof(int), 1, fin);
    fread(&n, sizeof(int), 1, fin);
    matrix = (double*)malloc(m*n*sizeof(double));
    fread(matrix, sizeof(double), m*n, fin);
    fclose(fin);

    //Modifying data
    for (i = 0; i < m*n; ++i) matrix[i] = 2*matrix[i];

    // Writing output file
    fout = fopen(argv[2], "wb");
    fwrite(&m, sizeof(int), 1, fout);
    fwrite(&n, sizeof(int), 1, fout);
    fwrite(matrix, sizeof(double), m*n, fout);
    fclose(fout);

    // De-allocate memory used for matrix.
    free(matrix);
    return 0;
 }

这个程序没有任何错误检查。你需要小心使用它,否则程序可能无法检测到文件,甚至无法分配所需的内存量。无论如何,我们可以使用您选择的编译器编译程序。
gcc -o genData genData.c

现在我们可以尝试使用这些函数从Mathematica中与两种语言进行通信。
matrix = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
writeDoubleMatrix[matrix, "MathematicaData.bin"];
Run["./genData MathematicaData.bin CData.bin"];
readDoubleMatrix["CData.bin"]

如果一切顺利,您应该得到的输出结果是:
{{2., 4., 6., 8.}, {10., 12., 14., 16.}, {18., 20., 22., 24.}}

是的,这是一种非常耗时的矩阵乘以2的方法,但这只是一个简单的示例,展示了如何从Mathematica到C,从C到Mathematica交换数据。

我不喜欢的是所有的数据都先存储到文件中,然后再在另一个程序中读取。有人能向我展示如何在不写入文件的情况下交换数据吗?我觉得管道可能是我需要的,但我不知道如何从任何一种语言中读取或写入它们。如果您可以修改此程序以适应管道,那将会很有帮助。


更新:

我发现如何使C程序“可管道化”。

//genDataPipe.c
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char** argv){
    int m, n, i;
    double* matrix;

    // Reading input file
    fread(&m, sizeof(int), 1, stdin);
    fread(&n, sizeof(int), 1, stdin);
    matrix = (double*)malloc(m*n*sizeof(double));
    fread(matrix, sizeof(double), m*n, stdin);

    //Modifying data
    for (i = 0; i < m*n; ++i) matrix[i] = 2*matrix[i];

    // Writing output file
    fwrite(&m, sizeof(int), 1, stdout);
    fwrite(&n, sizeof(int), 1, stdout);
    fwrite(matrix, sizeof(double), m*n, stdout);

    // Deallocate memory used for matrix.
    free(matrix);
    return 0;
}

这意味着我们必须像这样使用程序:
./genDataPipe < fileIn.bin > fileOut.bin

我一直在Mathematica文档中搜索,但我发现我只能通过管道外部命令来打开文件,例如OpenWrite
如果计算机系统支持管道,则OpenWrite["!command"]会运行指定的外部程序command,并打开一个管道以将输入发送到该程序。
这意味着我可以直接将二进制输入传递给C程序。问题是我找不到一种重定向程序输出的方法。我的解决方法是,将数据写入文件,并创建一个包装器来运行外部命令并读取外部命令输出内容。这里我们假设之前存在writeDoubleMatrix
getDataPipe[fileName_] := Module[{file, obj, m, n},
  file = OpenRead["!./genDataPipe < " <> fileName, 
  BinaryFormat -> True];
  m = BinaryRead[file, "Integer32"];
  n = BinaryRead[file, "Integer32"];
  obj = BinaryReadList[file, "Real64", m*n];
  Close[file];
  Partition[obj, n]
 ]
matrix = {{1, 2, 3}, {4, 5, 6}};
writeDoubleMatrix[matrix, "MData.bin"];
output = getDataPipe["MData.bin"]

这导致输出具有以下内容 {{2., 4., 6.}, {8., 10., 12.}}

现在唯一剩下的目标是找出如何消除writeDoubleMatrix的需要,并直接传递数据而无需编写文件。


请参考http://linux.die.net/man/1/mkfifo。 - Artefacto
你为什么不使用MathLink呢? - Joshua Martell
@Joshua,我可能可以找到如何使用它的方法,但我真的希望这两个程序是彼此独立的。我不想在Mathematica中与数据类型进行太多操作。我只想要一个自包含的东西。如果代码行数不太多,你能否将MathLink调整为我写的示例? - jmlopez
很奇怪,我找不到一个可以同时指定程序的标准输入和输出的函数。我认为你的选择是至少使用一个文件(广义上的;最好是命名管道)或使用一个MathLink包装器作为Mathematica和C程序之间的桥梁。 - Artefacto
@Artefacto,我也觉得很奇怪。我曾经在Python中使用过管道,这让我想知道为什么Mathematica没有实现类似的功能,或者如果他们实现了,为什么没有文档记录。我的意思是,将输入通过管道发送到外部程序,而无法获取输出的目的是什么?这会强制你让程序写入文件,不是吗? - jmlopez
4个回答

5
第一步是创建一个命名管道,就像Artefacto的评论中提到的那样。这称为fifo,即“先进先出”。一个管道将一个文件的输出变成另一个文件的输入。在开始之前,我想指出这些方法只适用于Linux。
基本上,管道的工作原理如下: mkfifo mypipe 或者在C中:system ("mkfifo mypipe"); 接下来的步骤是将输出写入管道,因为Linux中的所有内容都是文件,所以您可以使用标准的i/o操作,甚至可以将标准输入重定向到管道中。你已经有了在Mathematica和C中进行输出的代码。因此,在将文件的输出写入管道后,Mathematica版本可以从管道中读取输入,将其乘以并在stdout或其他地方显示它。您实际上不应该遇到这种方法的问题,因为在读取后,管道将被清空,并且在完成后可以轻松删除。如果要在之后删除管道,请运行system ("rm myfifo");
如果您真的不希望有任何辅助文件,尽管这并不算太糟糕,请尝试制作另一个实际上在标准输出中输出的文件,然后制作一个让Matematica从标准输入中读取的文件。现在,通过管道: ./cprogram | ./mprogram 这意味着您的C程序的输出应该是Mathematica程序的输入。据我所知,这仍然会创建一个管道,但在完成时系统会自动将其删除,并且普通用户可能看不到它。

在Windows下是否有可能做到这样的事情? - Alexey Popkov
@Alexey,这个链接是否回答了你的问题?我没有Windows机器,所以无法尝试。顺便说一下,现在我只需要找出如何在Mathematica中重定向输入和输出。 - jmlopez
如果我理解正确的话,@Johnathon告诉我们在Linux下可以创建并使用一个永久管道,具有可重用的读取写入功能,供Mathematica使用。据我所知,在Windows下的命令重定向操作符只允许输入/输出重定向一次,然后程序必须结束。我的问题是:在Windows下是否可能创建并使用这样的永久管道? - Alexey Popkov
@Alexey 对于Windows来说,没有标准的方法来创建命名管道。实际上,Windows的“命名管道”不能被任何实用程序创建,并且被用作套接字。也许您应该在%TEMP%下创建一个新文件,并使用‘tmpname’和‘tmpfile’来制作一个临时文件,然后将I/O重定向到那里。总之,在Windows中无法创建命名管道。 - Arka
@jmlopez 好的,这是我处理该问题的方法:您已经有了用C写入stdin的代码(虽然比我本来做的要复杂一些)。现在,下一步是从Mathematica获取输入,使C的输出成为Mathematica的输入。使用input_string = InputString[] ,我们可以从C程序中获取输入,这将是要转换为双倍的矩阵。然后,您可以像处理真实文件一样操作 input_string ,将每个元素都乘以两倍,然后打印输出。只需运行./cprogram | ./mprogram 即可完成。 - Arka
显示剩余3条评论

4
Mathematica中与同时从外部程序读取和写入的最接近的功能是RunThrough。根据文档说明,它不会像您想要的那样操作:通过WriteRead操作传递信息。相反,它将第二个参数写入临时文件,执行指定在第一个参数中的命令,并捕获命令的输出。这显然不是您要求的,但似乎是内置命令中最接近的。个人建议使用MathLink,但由于您似乎不想这样做,Johnathon's的答案是正确的方向。
为了更详细地解释他的答案,文档暗示Mathematica无法同时读取和写入同一文件/管道。这启发我想到了一个迂回的解决方案。首先,您需要2个管道:一个用于写入,一个用于读取,可以通过Run创建,例如:
Run["mkfifo in"]
Run["mkfifo out"].

第二步,打开输入/输出管道。
instrm = OpenRead["in"]
outstrn = OpenWrite["out"].

最后,如果外部程序已设置好处理命令行参数,那么通过将管道的名称传递给它来运行它。
Run["prog in out"],

或通过重定向
Run["prog <in >out"].

1
这与我在示例中编写的非常相似。我想我们可以得出结论,在Mathematica中无法重定向子进程的输入和输出。如果他们看一下Python如何处理管道,那就太好了。 - jmlopez
不,文档明确表示它不能重定向两者。 - rcollyer
1
@rcollyer 在我的MacOSX上使用mma 9.0.0时,我无法执行OpenRead["in"]并且OpenWrite["out"]会永久挂起我的内核。另请参见此问题,也许您可以回答那个问题。我认为必须有可能为OpenRead设置一个Method并仍然成功,但是到目前为止我还没有成功:). - Jacob Akkerboom
@JacobAkkerboom 我还没有尝试过这个,无论是v9还是其他任何版本。我会尝试一下。 - rcollyer

3

这有点尴尬,但你可以使用JLink:

Needs["JLink`"];
InstallJava[];
LoadJavaClass["java.lang.Runtime"];
runtime = java`lang`Runtime`getRuntime[];
process = runtime@exec["my program"];
input = process@getInputStream[];
output = process@getOutputStream[];

现在,您可以向另一端的标准输入管道写入内容:
output@write[ToCharacterCode["hi!"]];

并从启动的程序的标准输出中读取:

nextByte = input@read[];

很抱歉,我不太清楚如何将Mathematica的二进制输入导入到进程中,然后再将输出管道返回到Mathematica。我想我稍后会检查一下。 - jmlopez

3
MathLink可能是您的最佳选择。Mathematica和C程序将作为单独的进程,并使用管道或共享内存进行通信。您需要查看Mathematica安装中的MathLink示例。有一个名为mprep的工具可以从模板文件生成粘合代码,您需要链接MathLink库,但在此之后,使用数据数组调用C函数非常容易。
MLPutReal64Array - 将数据数组发送到Mathematica
MLGetReal64Array - 从Mathematica读取数组
我建议您选择其中一个示例并从那里开始工作。您可能需要Manual ReturnType和Real64List ArgumentType。

Joshua - 我查看了一些示例,但是它们似乎非常受限制。因为它们只支持某些类型,如果它们支持其他类型,则不太清楚如何操作。我宁愿使用自己的格式编写输出,这样我就知道如何在C中读取它,然后找到一种方法将其存储回Mathematica中。最终,我认为我会像MathLink一样做,但效率会更低,因为我没有共享内存。 - jmlopez
2
@jmlopez,我不确定你所说的“限制性”是什么意思。事实上,MathLink允许你的C程序与Mathematica内核交换任意的Mathematica表达式(即任何内容)。这确实是在Mathematica和C之间通信的正确工具。 - Mark McClure
@Mark,我担心的是列表中的嵌套列表。而且这些列表可能不是相同类型的对象。无论如何,我想我会尝试使用MathLink来编写上述示例,一旦我弄清楚了,就会在这里发布结果。 - jmlopez
MathLink可以向C发送一般表达式。这需要一些额外的工作,因为您必须询问接下来是什么并读取正确的内容,但它是完全通用的。 - Joshua Martell

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