9

I have a C++ program that uses R API to send commands to R and display the result. Once reduced to it's minimal form, it's an R console coded in C++. It used to work fine (most of the time) with R.3.4.3 and R.3.4.4, but everything falls appart when I tried to make the transition to R.3.5.1. For some commands (typically a call to "par", or "barplot", or anything related to graphics), I get the error message: "Error in < My command > : the base graphics system is not registered"

I never encountered this error before, and google searching it gives surprisingly few results.

My console using R3.4.3 (it's the same with 3.4.4) : Console use under R3.4.3

The same commands using R3.5.1: Console use under R3.5.1

Note that this behavior does not happen in a regular R console, so it must have something to do with the R-API for C/C++ (and the way it handles graphics devices, maybe?).

My code consists essentially in a RManager class that calls the API to exchange with R, and a simple window providing a lineEdit where user can input its command, and a text field where R results are displayed (see images above).

I'll provide the full code for reproducibility, but if you want to jump where R communication is really handled, it all happens in rmanager.cpp, the rest is just the GUI.

main.cpp:

#include "mainwindow.h"
#include <QApplication>
#include <thread>
#include <iostream>
#include <chrono>

#ifdef _WIN32
#include <windows.h>
#include <tlhelp32.h>
#include <QProcess>
#include <cwchar>
#endif


int main(int argc, char *argv[])
{
    int result = 0;

    QApplication a(argc, argv);
    MainWindow w;

    w.show();

    result = a.exec();

    return result;
}

MainWindow.h:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QTextStream>
#include <QFile>
#include <QTimer>
#include <rmanager.h>


namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    // Command received from GUI
    void on_inputCmd_returnPressed();
    // Read result from the R Manager
    void getResult(QString);
    //Read errors from the R Manager
    void getError(QString);

private:
    Ui::MainWindow *ui;

    QTimer pollInput;
    RManager *rconsole;

    // Result buffer for last command
    QString resultBuffer;

    // Send command directly to R
    void sendRCmd(QString command);

signals:
    // Starts the R Manager event loop
    void runConsole();
};

#endif // MAINWINDOW_H

mainWindow.cpp:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);  // Just a QLineEdit for inputs and a QTextEdit to display result.

    ui->outputConsole->document()->setMaximumBlockCount(500);

    // R API connection
    rconsole = new RManager(parent);

    // Signals connection
    connect(rconsole,   SIGNAL(writeConsole(QString)),  this,       SLOT(getResult(QString)));
    connect(rconsole,   SIGNAL(writeConsoleError(QString)), this,   SLOT(getError(QString)));
    connect(this,       SIGNAL(runConsole()),           rconsole,   SLOT(runConsole()));

    pollInput.start(10);    // Check for R results every 10 ms.

    // R Callbacks event loop
    emit runConsole();
}

MainWindow::~MainWindow()
{
    delete ui;
    pollInput.stop();
}

/**
 * @brief MainWindow::getResult Aggregate results from R until an only '\n' is sent
 * Then send it to the user (RPP or GUI)
 * @param res
 */
void MainWindow::getResult(QString res)
{
    // While != "\n" add res to the result buffer
    if (res != "\n") {
        resultBuffer.append(res);
    } else {
        // the res to the resultBuffer to conserve the last \n
        resultBuffer.append(res);

        // Get the current text values from the text fields and append the result
        ui->outputConsole->append(resultBuffer);

        resultBuffer.clear();
    }
}

/**
 * @brief MainWindow::getError Send immediatly any error from R
 * @param error
 */
void MainWindow::getError(QString error)
{
    qDebug() << "getError called with error: " << error ;

    // Get the current text values from the text fields and append the result
    ui->outputConsole->append(error);
}

/**
 * @brief MainWindow::sendRCmd Low level method to send command to R
 * Display the command in the GUI
 * @param command
 */
void MainWindow::sendRCmd(QString command)
{
    ui->outputConsole->append("> "+command+"\n");

    // Send the command to R
    rconsole->parseEval(command);

    ui->inputCmd->clear();
}


/**
 * @brief MainWindow::on_inputCmd_returnPressed Send command to R from the GUI
 */
void MainWindow::on_inputCmd_returnPressed()
{
    // Get the current text values from the text fields
    QString command = ui->inputCmd->text();
    sendRCmd(command);
}

ui_mainwindow.h (generated by Qt Creator):

/********************************************************************************
** Form generated from reading UI file 'mainwindow.ui'
**
** Created by: Qt User Interface Compiler version 5.11.0
**
** WARNING! All changes made in this file will be lost when recompiling UI file!
********************************************************************************/

#ifndef UI_MAINWINDOW_H
#define UI_MAINWINDOW_H

#include <QtCore/QVariant>
#include <QtWidgets/QApplication>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QMainWindow>
#include <QtWidgets/QTextEdit>
#include <QtWidgets/QVBoxLayout>
#include <QtWidgets/QWidget>

QT_BEGIN_NAMESPACE

class Ui_MainWindow
{
public:
    QWidget *centralWidget;
    QVBoxLayout *verticalLayout;
    QTextEdit *outputConsole;
    QLineEdit *inputCmd;

    void setupUi(QMainWindow *MainWindow)
    {
        if (MainWindow->objectName().isEmpty())
            MainWindow->setObjectName(QStringLiteral("MainWindow"));
        MainWindow->resize(382, 413);
        centralWidget = new QWidget(MainWindow);
        centralWidget->setObjectName(QStringLiteral("centralWidget"));
        verticalLayout = new QVBoxLayout(centralWidget);
        verticalLayout->setSpacing(6);
        verticalLayout->setContentsMargins(11, 11, 11, 11);
        verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
        outputConsole = new QTextEdit(centralWidget);
        outputConsole->setObjectName(QStringLiteral("outputConsole"));
        outputConsole->setFocusPolicy(Qt::NoFocus);
        outputConsole->setUndoRedoEnabled(false);
        outputConsole->setReadOnly(true);

        verticalLayout->addWidget(outputConsole);

        inputCmd = new QLineEdit(centralWidget);
        inputCmd->setObjectName(QStringLiteral("inputCmd"));
        inputCmd->setClearButtonEnabled(true);

        verticalLayout->addWidget(inputCmd);

        MainWindow->setCentralWidget(centralWidget);

        retranslateUi(MainWindow);

        QMetaObject::connectSlotsByName(MainWindow);
    } // setupUi

    void retranslateUi(QMainWindow *MainWindow)
    {
        MainWindow->setWindowTitle(QApplication::translate("MainWindow", "MainWindow", nullptr));
    } // retranslateUi

};

namespace Ui {
    class MainWindow: public Ui_MainWindow {};
} // namespace Ui

QT_END_NAMESPACE

#endif // UI_MAINWINDOW_H

rmanager.h

#ifndef RMANAGER_H
#define RMANAGER_H

#include <QObject>

class RManager : public QObject
{
    Q_OBJECT
public:
    explicit RManager(QObject *parent = 0);

    // R side methods/callbacks
    int parseEval(const QString & line);

    // R interface callbacks
    void myShowMessage( const char* message );
    void myWriteConsoleEx( const char* message, int len, int oType );
    int  myReadConsole(const char *, unsigned char *, int, int);
    int  winReadConsole(const char*, char*, int, int);
    void myResetConsole();
    void myFlushConsole();
    void myCleanerrConsole();
    void myBusy( int which );

    static RManager &r();

signals:
    void writeConsole(QString);
    void writeConsoleError(QString);

public slots:
    void runConsole();

private:
    bool R_is_busy;
    static RManager *r_inst;
};

// Functions to match the library : call RManager's methods
void myR_ShowMessage( const char* message );
void myR_WriteConsoleEx( const char* message, int len, int oType );
int  myR_ReadConsole(const char *prompt, unsigned char *buf, int len, int addtohistory);
int  ReadConsole(const char *prompt, char *buf, int len, int addtohistory);
void myR_ResetConsole();
void myR_FlushConsole();
void myR_ClearerrConsole();
void myR_Busy( int which );

void myR_CallBack();
void myR_AskOk(const char *);
int  myR_AskYesNoCancel(const char *);

#endif // RMANAGER_H

And finally, rmanager.cpp

#include "rmanager.h"

#include <qmessagebox.h>
#include <QDebug>

#define R_INTERFACE_PTRS

#include <Rembedded.h>
#ifndef _WIN32
#include <Rinterface.h>     // For Linux.
#endif
#include <R_ext/RStartup.h>
#include <Rinternals.h>
#include <R_ext/Parse.h>
#include <locale.h>

RManager* RManager::r_inst = 0 ;

RManager::RManager(QObject *parent) : QObject(parent)
{
    if (r_inst) {
        throw std::runtime_error( tr("Il ne peut y avoir qu'une instance de RppConsole").toStdString() ) ;
    } else {
        r_inst = this ;
    }

    const char *argv[] = {"RConsole", "--gui=none", "--no-save",
                        "--silent", "--vanilla", "--slave"};
    int argc = sizeof(argv) / sizeof(argv[0]);

    setlocale(LC_NUMERIC, "C"); //try to ensure R uses .

#ifndef _WIN32
    R_SignalHandlers = 0;               // Don't let R set up its own signal handlers
#endif

    Rf_initEmbeddedR(argc, (char**)argv);      // The call that is supposed to register the graphics system, amongst other things.

    R_ReplDLLinit();                    // this is to populate the repl console buffers

    structRstart Rst;
    R_DefParams(&Rst);
    Rst.R_Interactive = (Rboolean) false;       // sets interactive() to eval to false
#ifdef _WIN32
    Rst.rhome = getenv("R_HOME");
    Rst.home = getRUser();
    Rst.CharacterMode = LinkDLL;
    Rst.ReadConsole = ReadConsole;
    Rst.WriteConsole = NULL;
    Rst.WriteConsoleEx = myR_WriteConsoleEx;
    Rst.CallBack = myR_CallBack;
    Rst.ShowMessage = myR_AskOk;
    Rst.YesNoCancel = myR_AskYesNoCancel;
    Rst.Busy = myR_Busy;
#endif
    R_SetParams(&Rst);

    // Assign callbacks to R's
#ifndef _WIN32
    ptr_R_ShowMessage = myR_ShowMessage ;
    ptr_R_ReadConsole = myR_ReadConsole;
    ptr_R_WriteConsoleEx = myR_WriteConsoleEx ;
    ptr_R_WriteConsole = NULL;
    ptr_R_ResetConsole = myR_ResetConsole;
    ptr_R_FlushConsole = myR_FlushConsole;
    ptr_R_ClearerrConsole = myR_ClearerrConsole;
    ptr_R_Busy = myR_Busy;


    R_Outputfile = NULL;
    R_Consolefile = NULL;
#endif

#ifdef TIME_DEBUG
    _earliestSendToRBool = false;
#endif
    Rf_endEmbeddedR(0);
}

RManager &RManager::r()
{
    return *r_inst;
}

void RManager::runConsole()
{
    // Start the event loop to get results from R
    R_ReplDLLinit();
    while (R_ReplDLLdo1() > 0) {}
}


/**
 * @brief RManager::parseEval is the core of this console, sending commands to R.
 * @param line
 * @return
 */
int RManager::parseEval(const QString &line) {
    ParseStatus status;
    SEXP cmdSexp, cmdexpr = R_NilValue;
    int i, errorOccurred, retVal=0;

    // Convert the command line to SEXP
    PROTECT(cmdSexp = Rf_allocVector(STRSXP, 1));
    SET_STRING_ELT(cmdSexp, 0, Rf_mkChar(line.toLocal8Bit().data()));
    cmdexpr = PROTECT(R_ParseVector(cmdSexp, -1, &status, R_NilValue));

    switch (status){
    case PARSE_OK:
        // Loop is needed here as EXPSEXP might be of length > 1
        for(i = 0; ((i < Rf_length(cmdexpr)) && (retVal==0)); i++){
            R_tryEval(VECTOR_ELT(cmdexpr, i), R_GlobalEnv, &errorOccurred);
            if (errorOccurred) {
                retVal = -1;
            }
        }
        break;
    case PARSE_INCOMPLETE:
        // need to read another line
        retVal = 1;
        break;
    case PARSE_NULL:
        Rf_warning(tr("%s: Etat d'analyse de commande : NULL (%d)\n").toStdString().data(), "RPPConsole", status);
        retVal = -2;
        break;
    case PARSE_ERROR:
        Rf_warning(tr("Erreur d'analyse de la commande : \"%s\"\n").toStdString().data(), line.toStdString().c_str());
        retVal = -2;
        break;
    case PARSE_EOF:
        Rf_warning(tr("%s: Etat d'analyse de commande : EOF (%d)\n").toStdString().data(), "RPPConsole", status);
        break;
    default:
        Rf_warning(tr("%s: Etat d'analyse de commande non documenté %d\n").toStdString().data(), "RPPConsole", status);
        retVal = -2;
        break;
    }
    UNPROTECT(2);
    return retVal;
}

// RManager callbacks implementation
void RManager::myShowMessage(const char *message)
{
    // Never called till now
    QMessageBox::information(qobject_cast<QWidget*>(parent()),QString(tr("Bonjour le monde")),QString(message),QMessageBox::Ok,QMessageBox::NoButton);
}

void RManager::myWriteConsoleEx(const char *message, int len, int oType)
{
    QString msg;

    if (len) {
        msg = QString::fromLocal8Bit(message, len);
        if(!oType)
            emit writeConsole(msg);
        else
            emit writeConsoleError(msg);
    }
}

int RManager::myReadConsole(const char* /*prompt*/, unsigned char* /*buf*/, int /*len*/, int /*addtohistory*/ ){
    return 0;
}

// For Windows, unsigned char is replaced by char
int RManager::winReadConsole(const char* /*prompt*/, char* /*buf*/, int /*len*/, int /*addtohistory*/ ){
    return 0;
}

void RManager::myResetConsole()
{

}

void RManager::myFlushConsole()
{

}

void RManager::myCleanerrConsole()
{

}

void RManager::myBusy( int which ){
    R_is_busy = static_cast<bool>( which ) ;
}

// Connects R callbacks to RManager static methods
void myR_ShowMessage( const char* message ){
    RManager::r().myShowMessage( message ) ;
}

void myR_WriteConsoleEx( const char* message, int len, int oType ){
    RManager::r().myWriteConsoleEx(message, len, oType);
}

int myR_ReadConsole(const char *prompt, unsigned char *buf, int len, int addtohistory){
    return RManager::r().myReadConsole( prompt, buf, len, addtohistory ) ;
}

int ReadConsole(const char *prompt, char *buf, int len, int addtohistory) {
    return RManager::r().winReadConsole( prompt, buf, len, addtohistory ) ;
}

void myR_ResetConsole(){
    RManager::r().myResetConsole();
}

void myR_FlushConsole(){
    RManager::r().myFlushConsole();
}

void myR_ClearerrConsole(){
    RManager::r().myCleanerrConsole();
}

void myR_Busy( int which ){
    RManager::r().myBusy(which);
}

void myR_CallBack() {
//    Called during i/o, eval, graphics in ProcessEvents
}

void myR_AskOk(const char* /*info*/) {

}

int myR_AskYesNoCancel(const char* /*question*/) {
    const int yes = 1;
    return yes;
}

Thank you in advance for your ideas on what the problem might be. Is it a R.3.5.1 bug, or is there something that I should have defined/connected and missed ? I read the R.3.5.1 changes description without finding a clue about it.

PS: I'm under windows 10, compiling with Microsoft Visual C++ Compiler 15.0 (32 bits), and using Qt 5.11.0 (for the GUI components).

PPS: Following user2554330's advice, I checked for calls to GEregisterSystem, that is supposed to set the graphics system, and thus prevent this error. I found that in both cases, this function is called, at application launch, but not with the same call stack.

For R.3.4.3: Call stack to GEregisterSystem for R3.4.3

For R.3.5.1: Call stack to GEregisterSystem for R3.5.1

Andéol Evain
  • 507
  • 2
  • 12
  • The graphics system is registered in a call to `GEregisterSystem`. I'd use the debugger to see if there are differences in your calls to that function in older R versions versus the current one. – user2554330 Jul 09 '18 at 11:51
  • Hello ! I tried your suggestion. I found out that GEregisterSystem is called in both cases (surprisingly, I expected it not to be called in 3.5.1). However, the call stack is not the same. I 'll update my post to add those info, and keep exploring that. – Andéol Evain Jul 09 '18 at 13:23

1 Answers1

0

I found a solution (thanks to Luke Tierney on R-devel mailing list). I just needed to move the call to Rf_endEmbeddedR to the destructor of RManager, where it should have been.

It doesn't really explain why it worked the way it was before with R.3.4 and not with R.3.5, but it does solve the practical issue. Maybe this was never supposed to work with Rf_endEmbeddedR called so soon, and it only used to, thanks to a bug that has been fixed.

Andéol Evain
  • 507
  • 2
  • 12