可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I think I am starting to understand how to link functions written in C/C++ to Mathematica. The problem I'm facing is that I don't know how to send error messages from my C wrapper to Mathematica. After searching in google I found this MathLink Tutorial.
Section 1.7 gave me an insight as to how to send error messages but I am getting weird results. Here is the code I am working with.
//File cppFunctions.h
#ifndef CPPFUNCTIONS_H
#define CPPFUNCTIONS_H
class Point {
public:
double x, y;
Point(){ x=y=0.0;}
Point(double a, double b): x(a), y(b) {}
};
class Line {
public:
Point p1, p2;
Line(void) {}
Line(const Point &P, const Point &Q): p1(P), p2(Q) {}
double distanceTo(const Line &M, const double &EPS = 0.000001){
double x21 = p2.x - p1.x; double y21 = p2.y - p1.y;
double x43 = M.p2.x - M.p1.x; double y43 = M.p2.y - M.p1.y;
double x13 = p1.x - M.p1.x; double y13 = p1.y - M.p1.y;
double den = y43*x21 - x43*y21;
if (den*den < EPS) return -INFINITY;
double numL = (x43*y13 - y43*x13)/den;
double numM = (x21*y13 - y21*x13)/den;
if (numM < 0 || numM > 1) return -INFINITY;
return numL;
}
};
#endif
The file cppFunctions.h declares the classes Point
and Line
. I have stripped this class to the bare minium except for the function that I want to use in Mathematica. I want to find the distance from one line to another. This function is the C version of lineInt
in wireframes in Mathematica. To use this function in Mathematica we need a wrapper function that obtains the input from Mathematica and sends the output back to Mathematica.
//mlwrapper.cpp
#include "mathlink.h"
#include <math.h>
#include "cppFunctions.h"
void ML_GetPoint(Point &P){
long n;
MLCheckFunction(stdlink, "List", &n);
MLGetReal64(stdlink, &P.x);
MLGetReal64(stdlink, &P.y);
}
void ML_GetLine(Line &L){
long n;
MLCheckFunction(stdlink, "List", &n);
ML_GetPoint(L.p1);
ML_GetPoint(L.p2);
}
double LineDistance(void) {
Line L, M;
ML_GetLine(L);
ML_GetLine(M);
return L.distanceTo(M);
}
int main(int argc, char* argv[]) {
return MLMain(argc, argv);
}
I created two helper functions: ML_GetPoint
and ML_GetLine
to help me obtain the input from Mathematica. A line is obtained from a list containing two list. Each sublist is a list of 2 real numbers (a point). To try this function in Mathematica we need to more files.
//mlwrapper.tm
double LineDistance P((void));
:Begin:
:Function: LineDistance
:Pattern: LineDistance[L_List, M_List]
:Arguments: {L, M}
:ArgumentTypes: {Manual}
:ReturnType: Real
:End:
:Evaluate: LineDistance::usage = "LineDistance[{{x1,y1}, {x2,y2}}, {{x3,y3}, {x4,y4}}] gives the distance between two lines."
:Evaluate: LineDistance::mlink = "There has been a low-level MathLink error. The message is: `1`"
This file states that the function LineDistance will obtain the arguments manually and that it will return a real number. The last two lines are important. The first Evaluate
declares the usage
of the function. It gives a brief message about the function when ?LineDistance
is entered into Mathematica. The other Evaluate
is the one I wish to use whenever there is an error (more on this later).
#Makefile
VERSION=8.0
MLINKDIR = .
SYS = MacOSX-x86-64
CADDSDIR = /Applications/Mathematica.app/SystemFiles/Links/MathLink/DeveloperKit/CompilerAdditions
INCDIR = ${CADDSDIR}
LIBDIR = ${CADDSDIR}
MPREP = ${CADDSDIR}/mprep
RM = rm
CXX = g++
BINARIES = mlwrapper
all : $(BINARIES)
mlwrapper : mlwrappertm.o mlwrapper.o
${CXX} -I${INCDIR} mlwrappertm.o mlwrapper.o -L${LIBDIR} -lMLi3 -lstdc++ -framework Foundation -o $@
.cpp.o :
${CXX} -c -I${INCDIR} $<
mlwrappertm.cpp : mlwrapper.tm
${MPREP} $? -o $@
clean :
@ ${RM} -rf *.o *tm.c mlwrappertm.cpp
Last file is the Makefile. At this point we are all set to test the function in Mathematica.
I should have mentioned before that I'm using Mac OS X, I'm not sure how this will work on Windows. In the mlwrapper.cpp the main function needs a lot more code which you can find in one of the examples provided by Mathematica.
In the terminal I know do this:
make > makelog.txt
make clean
This make the executable mlwrapper
. Now we can start using Mathematica:
SetDirectory[NotebookDirectory[]];
link = Install["mlwrapper"];
?LineDistance
Manipulate[
Grid[{{
Graphics[{
Line[{p1, p2}, VertexColors -> {Red, Red}],
Line[{p3, p4}]
},
PlotRange -> 3, Axes -> True],
LineDistance[{p1, p2}, {p3, p4}]
}}],
{{p1, {-1, 1}}, Locator, Appearance -> "L1"},
{{p2, {2, 1}}, Locator, Appearance -> "L2"},
{{p3, {2, -2}}, Locator, Appearance -> "M1"},
{{p4, {2, 3}}, Locator, Appearance -> "M2"}
]
The output we obtain is the following:
Everything works fine as long as you enter the correct arguments. That is, 2 lists, each one being a list of 2 lists of 2 doubles. Maybe there is another way of obtaining the inputs, if you know how to please let me know. If we stick to this method all we need is a way of letting the Mathematica user know if there are any errors. A very simple one is entering the incorrect input. Lets say I enter this:
LineDistance[{{0, 0}, {0}}, {{1, -1}, {1, 1}}]
The output is $Failed
. How about the following:
LineDistance[{{1, -1}, {1, 1}}]
The output is LineDistance[{{1, -1}, {1, 1}}]
. I'm guessing this happens because we described in Pattern
section of the .tm
that the function accepts two lists and since we only gave one it doesn't match the pattern. Is this true?
In any case, following the tutorial I found lets modify the file mlwrapper.cpp as follows:
#include "mathlink.h"
#include <math.h>
#include <string>
#include "cppFunctions.h"
bool ML_Attempt(int func, const char* err_tag){
if (!func) {
char err_msg[100];
sprintf(err_msg, "Message[%s,\"%.76s\"]", err_tag, MLErrorMessage(stdlink));
MLClearError(stdlink); MLNewPacket(stdlink); MLEvaluate(stdlink, err_msg);
MLNextPacket(stdlink); MLNewPacket(stdlink); MLPutSymbol(stdlink, "$Failed");
return false;
}
return true;
}
void ML_GetPoint(Point &P){
long n;
if(!ML_Attempt(MLCheckFunction(stdlink, "List", &n), "LineDistance::mlink2"))return;
if(!ML_Attempt(MLGetReal64(stdlink, &P.x), "LineDistance::mlink3")) return;
if(!ML_Attempt(MLGetReal64(stdlink, &P.y), "LineDistance::mlink4")) return;
}
void ML_GetLine(Line &L){
long n;
if(!ML_Attempt(MLCheckFunction(stdlink, "List", &n), "LineDistance::mlink1"))return;
ML_GetPoint(L.p1);
ML_GetPoint(L.p2);
}
double LineDistance(void) {
Line L, M;
ML_GetLine(L);
ML_GetLine(M);
return L.distanceTo(M);
}
int main(int argc, char* argv[]) {
return MLMain(argc, argv);
}
And add the following to the end of the mlwrapper.tm file
:Evaluate: LineDistance::mlink1 = "There has been a low-level MathLink error. The message is: `1`"
:Evaluate: LineDistance::mlink2 = "There has been a low-level MathLink error. The message is: `1`"
:Evaluate: LineDistance::mlink3 = "There has been a low-level MathLink error. The message is: `1`"
:Evaluate: LineDistance::mlink4 = "There has been a low-level MathLink error. The message is: `1`"
Now lets use make and try to make some mistakes in Mathematica.
I'm posting a screenshot of what output instead of writing everything.
Notice how we get different errors after we repeat the call. It seems that the function continues at the line after the error was encountered. If I don't use any of the other ML functions like in the function ML_Attempt
and I only use the MLEvaluate
to send the error tag then the MathLink is broken and I have to re-install the link. Does anyone know how to send error messages to Mathematica from C?
UPDATE:
Based on the answers that have been given and another useful document (Chapter 8) I managed to make it work. The code isn't so pretty at the moment but this made me ask the following question. Is it possible to terminate a function earlier? In a regular C program if I encounter an error I would print an error message and use the exit
function. Can we do something similar? If we use the exit
function the link will be broken and we will have to re-install the function. Take the functions ML_GetPoint
and ML_GetLine
for example. If an error occurred here then there is no point on procedding doing the calculations in the main function LineDistance
. We need to clear whatever error we have obtained, send a message to Mathematica specifying the error, quit for now and wait for the next call.
回答1:
As an alternative to the solution by @ragfield, you may declare your function as void
and return the result manually. Here is an example based on addTwo
standard example. Here is the template:
void addtwo P(( int, int));
:Begin:
:Function: addtwo
:Pattern: AddTwo[i_Integer, j_Integer]
:Arguments: { i, j }
:ArgumentTypes: { Integer, Integer }
:ReturnType: Manual
:End:
:Evaluate: AddTwo::usage = "AddTwo[x, y] gives the sum of two machine
integers x and y."
:Evaluate: AddTwo::badargs = "Arguments are expected to be positive numbers"
and the function:
void addtwo( int i, int j) {
if(i>0&&j>0){
MLPutInteger(stdlink,i+j);
}else{
MLPutFunction(stdlink,"CompoundExpression",2);
MLPutFunction(stdlink,"Message",1);
MLPutFunction(stdlink,"MessageName",2);
MLPutSymbol(stdlink,"AddTwo");
MLPutString(stdlink,"badargs");
MLPutSymbol(stdlink,"$Failed");
}
}
Here are examples of use:
In[16]:= AddTwo[1,2]
Out[16]= 3
In[17]:= AddTwo[-1,2]
During evaluation of In[17]:= AddTwo::badargs: Arguments are expected
to be positive numbers
Out[17]= $Failed
This is a little more "high level" way to do it, in this way you don't have to deal with packets explicitly.
However, I feel that the full error-checking of the input arguments better be performed on the Mathematica side through appropriate patterns, and the option of error messages saved for some internal errors detected in your C code (I actually go further and return to Mathematica just error codes as integers or strings, and let higher-level mma wrappers deal with them and issue messages). You can place those patterns in your template file, or you can wrap your MathLink Mathematica function into another function that will perform the error-checking. I have a very limited experience with Mathlink though, so my opinion here should not perhaps count much.
EDIT
Per request in the comment (although I wasn't sure that I understood the request correctly):
extern void addeight( void );
extern void addall(void);
static void putErrorMessageAndReturnFailure(const char *fname, const char *msgtag);
void addeight(void)
{
int i,j,k,l,i1,j1,k1,l1;
MLGetInteger(stdlink,&i);
MLGetInteger(stdlink,&j);
MLGetInteger(stdlink,&k);
MLGetInteger(stdlink,&l);
MLGetInteger(stdlink,&i1);
MLGetInteger(stdlink,&j1);
MLGetInteger(stdlink,&k1);
MLGetInteger(stdlink,&l1);
if(i<0||j<0||k<0||l<0||i1<0||j1<0||k1<0||l1<0){
putErrorMessageAndReturnFailure("AddEight","badargs");
}else{
MLPutFunction(stdlink,"List",2);
MLPutFunction(stdlink,"List",2);
MLPutInteger(stdlink,i+i1);
MLPutInteger(stdlink,j+j1);
MLPutFunction(stdlink,"List",2);
MLPutInteger(stdlink,k+k1);
MLPutInteger(stdlink,l+l1);
}
}
void addall(){
int *data, len, i = 0,sum = 0;
if(!MLGetIntegerList(stdlink, &data, &len)){
putErrorMessageAndReturnFailure("AddAll","interr");
return;
}
for(i=0;i<len;i++){
if(data[i]<0){
putErrorMessageAndReturnFailure("AddAll","badargs");
return;
}else{
sum+=data[i];
}
}
MLPutInteger(stdlink,sum);
MLReleaseInteger32List(stdlink, data, len);
}
static void putErrorMessageAndReturnFailure(const char *fname, const char *msgtag){
MLPutFunction(stdlink,"CompoundExpression",2);
MLPutFunction(stdlink,"Message",1);
MLPutFunction(stdlink,"MessageName",2);
MLPutSymbol(stdlink,fname);
MLPutString(stdlink,msgtag);
MLPutSymbol(stdlink,"$Failed");
}
and the template
void addeight P(( ));
:Begin:
:Function: addeight
:Pattern: AddEight[{{i_Integer, j_Integer},{k_Integer,l_Integer}},{{i1_Integer,j1_Integer},{k1_Integer,l1_Integer}}]
:Arguments: { i, j, k ,l, i1,j1,k1,l1 }
:ArgumentTypes: { Manual }
:ReturnType: Manual
:End:
:Evaluate: AddEight::usage = "AddEight[{{i_Integer, j_Integer},{k_Integer,l_Integer}}, {{i1_Integer, j1_Integer},{k1_Integer,l1_Integer}}] gives the sum as a list: {{i+i1,j+j1},{k+k1,l+l1}}."
:Evaluate: AddEight::badargs = "Arguments are expected to be positive numbers"
void addall P(( ));
:Begin:
:Function: addall
:Pattern: AddAll[fst:{{_Integer, _Integer},{_Integer,_Integer}},sec:{{_Integer, _Integer},{_Integer,_Integer}}]
:Arguments: { Flatten[{fst,sec}]}
:ArgumentTypes: { Manual }
:ReturnType: Manual
:End:
:Evaluate: AddAll::usage = "AddAll[{{i_Integer, j_Integer},{k_Integer,l_Integer}},{{i1_Integer, j1_Integer},{k1_Integer,l1_Integer}}] gives the total sum of elemens of the sub-lists."
:Evaluate: AddAll::badargs = "Arguments are expected to be positive numbers"
:Evaluate: AddAll::interr = "Internal error - error getting the data from Mathematica"
Examples:
In[8]:= AddEight[{{1,2},{3,4}},{{5,6},{7,8}}]
Out[8]= {{6,8},{10,12}}
In[9]:= AddEight[{{-1,2},{3,4}},{{5,6},{7,8}}]
During evaluation of In[9]:= AddEight::badargs: Arguments are expected to be positive numbers
Out[9]= $Failed
In[10]:= AddAll[{{1,2},{3,4}},{{5,6},{7,8}}]
Out[10]= 36
In[11]:= AddAll[{{-1,2},{3,4}},{{5,6},{7,8}}]
During evaluation of In[11]:= AddAll::badargs: Arguments are expected to be positive numbers
Out[11]= $Failed
回答2:
Something like this usually works for me:
void putMessage(const char* messageSymbol, const char* messageTag, const char* messageParam)
{
MLNewPacket(stdlink);
MLPutFunction(stdlink, "EvaluatePacket", 1);
MLPutFunction(stdlink, "Message", 2);
MLPutFunction(stdlink, "MessageName", 2);
MLPutSymbol(stdlink, messageSymbol);
MLPutString(stdlink, messageTag);
MLPutString(stdlink, messageParam);
MLFlush(stdlink);
MLNextPacket(stdlink);
MLNewPacket(stdlink);
}
You'll still have to return a result, e.g.
MLPutSymbol(stdlink, "$Failed");
回答3:
This post is for anybody with interest in how I wrote my final code. This code is based on the helpful discussions with @Leonid. Lets start with a utility file.
//MLErrors.h
#include <stdarg.h>
#include <vector>
#include <sstream>
#define CCHAR const char*
#define UINT unsigned int
class MLException {
public:
CCHAR sym;
CCHAR tag;
std::vector<std::string> err;
MLException(CCHAR msgSym, CCHAR msgTag, UINT n, ...):
sym(msgSym), tag(msgTag), err(n)
{
std::stringstream ss;
va_list args;
va_start(args, n);
for (UINT i=0; i < n; ++i) {
err[i] = va_arg(args, CCHAR);
if (err[i][0] == '%') {
switch (err[i][1]) {
case 'i':
ss << va_arg(args, int);
break;
case 'd':
ss << va_arg(args, double);
break;
default:
break;
}
err[i] = ss.str();
}
}
va_end(args);
}
};
#undef CCHAR
#undef UINT
void ML_SendMessage(const MLException& e){
if (MLError(stdlink) != MLEOK) MLClearError(stdlink);
MLNewPacket(stdlink);
MLPutFunction(stdlink, "EvaluatePacket", 1);
MLPutFunction(stdlink, "Message", e.err.size()+1);
MLPutFunction(stdlink, "MessageName", 2);
MLPutSymbol(stdlink, e.sym);
MLPutString(stdlink, e.tag);
for (int i=0; i < e.err.size(); ++i) {
MLPutString(stdlink, e.err[i].c_str());
}
MLFlush(stdlink);
MLNextPacket(stdlink);
MLNewPacket(stdlink);
MLPutSymbol(stdlink, "$Failed");
}
This file contains the MLException
class and the function ML_SendMessage
. Here is the simple explanation. An object of the type MLException
contains 2 strings and a vector of strings. If e
is an MLException
then e.sym
must be a valid Mathematica symbol and e.tag
a valid tag. We define this Symbol::tag
in the template file. If the Symbol::tag
contains parameters then they are stored in e.err
. For instance say that you declared the following in the template file:
:Evaluate: someSymbol::someTag = "Error, the program encountered: `1`, it needed `2`."
Then if some for reason there is an error in the C function you can get out of there by throwing an exception with some message. Like this:
if(ERROR) throw MLException("someSymbol", "someTag", 2, "this", "that");
Notice how the 3rd argument is an integer. This is the number of messages that will be placed instead of the "1" and "2" in the message. This means that the message that you will see in Mathematica is: "Error, the program encountered: this, it needed that." If you need to include numbers in the message I made it so that you write a string followed by a number. For instance, if you want to write the number 100 and then some other message then you can throw the exception like this:
if(ERROR) throw MLException("someSymbol", "someTag", 2, "%i", 100, "msg");
If you want to include a double then use "%d" instead.
The ML_sendMessage
function takes in the exception, clears the errors, sends a message to Mathematica and puts $Failed
.
Here is my template file:
//mlwrapper.tm
:Evaluate: BeginPackage["mlwrapper`"]
:Evaluate: EMPH[a_] := ToString[Style[a, "TI"], StandardForm]
:Evaluate: LINK[url_, label_ : Style["\[RightSkeleton]", "SR"]] := ToString[Hyperlink[label, url], StandardForm]
:Evaluate: LineDistance::usage = "LineDistance["<>EMPH["L"]<>", "<>EMPH["M"]<>"] gives the distance between two lines. "<>LINK["#"]
:Evaluate: mlwrapper::mlink = "There has been a low-level MathLink error. The message is: `1`"
:Evaluate: GetPoint::narg = "A point is a list of 2 numbers. A list of `1` elements was passed instead. "<>LINK["#"]
:Evaluate: GetLine::narg = "A line is a list of 2 points. A list of `1` elements was passed instead. "<>LINK["#"]
:Evaluate: EndPackage[]
:Evaluate: Begin["mlwrapper`Private`"]
void LineDistance P((void));
:Begin:
:Function: LineDistance
:Pattern: LineDistance[L_List, M_List]
:Arguments: {L, M}
:ArgumentTypes: {Manual}
:ReturnType: Manual
:End:
:Evaluate: End[]
I decided to make this into a package. I also declared functions EMPH
and LINK
. The first one emphasizes words and the other one allows us to write hyperlinks. Once I learn how to properly document I will adding hyperlinks to the descriptions.
Now we can describe errors in the same way we would in Mathematica:
//mlwrapper.cpp
#include "mathlink.h"
#include "MLErrors.h"
#include <cmath>
#include "cppFunctions.h"
#define MLINKERROR MLException("mlwrapper", "mlink", 1, MLErrorMessage(stdlink))
void ML_GetPoint(Point &P){
long n = 0;
MLCheckFunction(stdlink, "List", &n);
if (n != 2) throw MLException("GetPoint", "narg", 1, "%i", n);
MLGetReal64(stdlink, &P.x);
MLGetReal64(stdlink, &P.y);
if (MLError(stdlink) != MLEOK) throw MLINKERROR;
}
void ML_GetLine(Line &L){
long n = 0;
MLCheckFunction(stdlink, "List", &n);
if (n != 2) throw MLException("GetLine", "narg", 1, "%i", n);
ML_GetPoint(L.p1);
ML_GetPoint(L.p2);
}
void LineDistance(void) {
Line L, M;
try {
ML_GetLine(L);
ML_GetLine(M);
}
catch (MLException& e) {
ML_SendMessage(e);
return;
}
MLPutReal64(stdlink, L.distanceTo(M));
}
int main(int argc, char* argv[]) {
return MLMain(argc, argv);
}
Now, since I'm making a package we need one last file: mlwrapper.m. In this file we add the this line: Install["mlwrapper"];
. We are assuming of course that the executable mlwrapper
is in the same directory. Finally, I show a screenshot of the results:
I wrote the last statement to have an idea of the overhead of the exceptions. I mainly want the exceptions to handle the acquisition of input and maybe some other errors based on return statements from the C/C++ function. In any case, writing a wrapper without exception didn't do much better than with exceptions.
So there you have it. Another example of how to call C/C++ functions from Mathematica.
I would also like to thank @alexey-popkov for giving me the idea to write EMPH
and LINK
. It was giving me a headache finding out how to format my messages.