Mathematica: сообщения об ошибках MathLink

Я думаю, что начинаю понимать, как связать функции, написанные на C/C++, с Mathematica. Проблема, с которой я сталкиваюсь, заключается в том, что я не знаю, как отправлять сообщения об ошибках из моей оболочки C в Mathematica. После поиска в Google я нашел этот учебник MathLink.

Раздел 1.7 дал мне понимание того, как отправлять сообщения об ошибках, но я получаю странные результаты. Вот код, с которым я работаю.


//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

Файл cppFunctions.h объявляет классы Point а также Line, Я раздвинул этот класс до минимума за исключением функции, которую я хочу использовать в Mathematica. Я хочу найти расстояние от одной линии до другой. Эта функция является C-версией lineInt в каркасах в Mathematica. Чтобы использовать эту функцию в Mathematica, нам нужна функция-обертка, которая получает входные данные от Mathematica и отправляет выходные данные обратно в 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);
}

Я создал две вспомогательные функции: ML_GetPoint а также ML_GetLine чтобы помочь мне получить вклад от Mathematica. Строка получается из списка, содержащего два списка. Каждый подсписок представляет собой список из 2 действительных чисел (точка). Чтобы попробовать эту функцию в Mathematica, нам нужно больше файлов.


//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`"

В этом файле говорится, что функция LineDistance будет получать аргументы вручную и возвращает действительное число. Последние две строки важны. Первый Evaluate объявляет usage функции. Это дает краткое сообщение о функции, когда ?LineDistance вводится в Mathematica. Другой Evaluate это тот, который я хочу использовать всякий раз, когда возникает ошибка (подробнее об этом позже).


#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

Последний файл - это Makefile. На этом этапе мы все готовы проверить функцию в Mathematica.


Я должен был упомянуть ранее, что я использую Mac OS X, я не уверен, как это будет работать в Windows. В mlwrapper.cpp основной функции требуется намного больше кода, который вы можете найти в одном из примеров, предоставленных Mathematica.

В терминале я знаю, сделать это:

make > makelog.txt
make clean

Это делает исполняемый файл mlwrapper, Теперь мы можем начать использовать 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"}

]

Вывод, который мы получаем, следующий:

Выход

Все работает нормально, пока вы вводите правильные аргументы. Таким образом, 2 списка, каждый из которых представляет собой список из 2 списков по 2 двойных. Может быть, есть другой способ получения входных данных, если вы знаете, как, пожалуйста, дайте мне знать. Если мы придерживаемся этого метода, все, что нам нужно, это способ сообщить пользователю Mathematica, есть ли какие-либо ошибки. Очень простой ввод неправильного ввода. Допустим, я вхожу в это:

LineDistance[{{0, 0}, {0}}, {{1, -1}, {1, 1}}]

Выход $Failed, Как насчет следующего:

LineDistance[{{1, -1}, {1, 1}}]

Выход LineDistance[{{1, -1}, {1, 1}}], Я предполагаю, что это происходит, потому что мы описали в Pattern раздел .tm что функция принимает два списка, и поскольку мы дали только один, это не соответствует шаблону. Это правда?

В любом случае, следуя найденному мною руководству, давайте изменим файл mlwrapper.cpp следующим образом:

#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);
}

И добавьте следующее в конец файла mlwrapper.tm

: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`"

Теперь давайте используем make и пытаемся сделать несколько ошибок в Mathematica.

Я выкладываю скриншот того, что вывод, вместо того, чтобы писать все.

Выход

Обратите внимание, как мы получаем разные ошибки после повторного вызова. Кажется, что функция продолжается в строке после ошибки. Если я не использую какие-либо другие функции ML, как в функции ML_Attempt и я использую только MLEvaluate чтобы отправить тег ошибки, MathLink не работает, и я должен переустановить ссылку. Кто-нибудь знает, как отправлять сообщения об ошибках в Mathematica из C?


ОБНОВИТЬ:

На основании полученных ответов и другого полезного документа (глава 8) мне удалось заставить его работать. На данный момент код не такой красивый, но это заставило меня задать следующий вопрос. Возможно ли завершить функцию раньше? В обычной C-программе, если я обнаружу ошибку, я напечатаю сообщение об ошибке и использую exit функция. Можем ли мы сделать что-то подобное? Если мы используем exit Функция ссылка будет разорвана, и нам придется переустановить функцию. Взять на себя функции ML_GetPoint а также ML_GetLine например. Если здесь произошла ошибка, то нет смысла в процедурах, выполняющих вычисления в основной функции. LineDistance, Нам нужно очистить полученную ошибку, отправить сообщение в Mathematica с указанием ошибки, выйти из системы и дождаться следующего вызова.

3 ответа

Решение

В качестве альтернативы решению @ragfield вы можете объявить свою функцию как void и вернуть результат вручную. Вот пример, основанный на addTwo стандартный пример. Вот шаблон:

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"

и функция:

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");
    }
}

Вот примеры использования:

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

Это немного более "высокоуровневый" способ сделать это, так что вам не нужно явно иметь дело с пакетами.

Однако я чувствую, что полную проверку ошибок входных аргументов лучше выполнять на стороне Mathematica с помощью соответствующих шаблонов, а также возможность сохранения сообщений об ошибках для некоторых внутренних ошибок, обнаруженных в вашем коде C (на самом деле я иду дальше и возвращаюсь к Mathematica просто коды ошибок в виде целых чисел или строк, и пусть обертки mma более высокого уровня обрабатывают их и выдают сообщения). Вы можете поместить эти шаблоны в файл шаблона или обернуть функцию MathLink Mathematica в другую функцию, которая будет выполнять проверку ошибок. У меня очень ограниченный опыт работы с Mathlink, поэтому мое мнение здесь, возможно, не должно иметь большого значения.

РЕДАКТИРОВАТЬ

За запрос в комментарии (хотя я не был уверен, что правильно понял запрос):

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");
}

и шаблон

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"

Примеры:

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

Нечто подобное обычно работает для меня:

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);
}

Вам все равно придется вернуть результат, например

MLPutSymbol(stdlink, "$Failed");

Этот пост предназначен для всех, кто интересуется тем, как я написал свой окончательный код. Этот код основан на полезных обсуждениях с @Leonid. Начнем с служебного файла.


//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");
}

Этот файл содержит MLException класс и функция ML_SendMessage, Вот простое объяснение. Объект типа MLException содержит 2 строки и вектор строк. Если e является MLException затем e.sym должен быть действительным символом Mathematica и e.tag действительный тег. Мы определяем это Symbol::tag в файле шаблона. Если Symbol::tag содержит параметры, то они хранятся в e.err, Например, скажем, что вы объявили следующее в файле шаблона:

:Evaluate: someSymbol::someTag = "Error, the program encountered: `1`, it needed `2`."

Затем, если по какой-либо причине есть ошибка в функции C, вы можете выйти из нее, вызвав исключение с каким-либо сообщением. Как это:

if(ERROR) throw MLException("someSymbol", "someTag", 2, "this", "that");

Обратите внимание, что третий аргумент является целым числом. Это количество сообщений, которые будут помещены вместо "1" и "2" в сообщении. Это означает, что сообщение, которое вы увидите в Mathematica: "Ошибка, обнаружена программа: это, это нужно, это". Если вам нужно включить числа в сообщение, я сделал это так, чтобы вы написали строку, за которой следовал номер. Например, если вы хотите написать число 100, а затем какое-то другое сообщение, вы можете выбросить исключение следующим образом:

 if(ERROR) throw MLException("someSymbol", "someTag", 2, "%i", 100, "msg");

Если вы хотите включить двойное значение, используйте вместо этого "%d".

ML_sendMessage Функция принимает исключение, очищает ошибки, отправляет сообщение в Mathematica и помещает $Failed,

Вот мой файл шаблона:

//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[]

Я решил сделать это в пакете. Я также объявил функции EMPH а также LINK, Первый подчеркивает слова, а второй позволяет нам писать гиперссылки. Как только я научусь правильно документировать, я добавлю гиперссылки к описаниям.

Теперь мы можем описать ошибки так же, как в 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);
}

Теперь, так как я делаю пакет, нам нужен один последний файл: mlwrapper.m. В этом файле мы добавляем эту строку: Install["mlwrapper"];, Мы, конечно, предполагаем, что исполняемый файл mlwrapper находится в том же каталоге. Наконец, я показываю скриншот результатов:

Выход

Я написал последнее заявление, чтобы иметь представление о накладных расходах исключений. В основном я хочу, чтобы исключения обрабатывали получение ввода и, возможно, некоторые другие ошибки, основанные на операторах возврата из функции C/C++. В любом случае, написание оболочки без исключений не принесло больших результатов, чем за исключением.

Так что у вас есть это. Еще один пример того, как вызывать функции C/C++ из Mathematica.

Я также хотел бы поблагодарить @alexey-popkov за предоставленную мне идею написать EMPH а также LINK, Это доставляло мне головную боль, узнавая, как форматировать мои сообщения.

Другие вопросы по тегам