Do the simplest thing possible, but not the simpler.

Albert Einstein



Wanna go
Home to part 1 to part 2

Embedding the BASIC interpreter in a C++ project

I think the better way to learn how an application works is reading its source code.

In this last part of the material about the BASIC interpreter, I will demonstrate how to integrate the interpreter engine in a C++ application, how to extend the interpreter functionalities (with no need to change the original files), and how to compile and run the C++ host application to check its results.

Take a look at the code below. It is a fully functional main file for a small C++ application integrating the BASIC interpreter.

            #include <algorithm> ///std::transform
#include <string> ///std::string
#include <iostream> ///std::cout
#include <sstream> ///std::ostringstream
#include <vector> ///std::vector
#include <map> ///std::map

using namespace std;

#include "basic.hpp"
#include "baslib.hpp"
#include "utils.hpp"

///main program entry point
int main(int argc, char *argv[]) {
    CBasic basic; ///BASIC interpreter object
    TProgramFunctionsDictionary funcs; ///extension functions dictionary
    int res; ///Compilation result
    ///Kernighan and Ritchie taught us that any first test program for a programming language
    ///must be the one that shows "Hello world." on screen.
    string bassrc("print 'Hello world.' : print\n"); ///BASIC program to compile/execute

    res = basic.Compile(bassrc, funcs); ///no external functions for the moment
    if (!res) {
        basic.ExecuteProgram();
        cout << endl;
    } else {
        cout << "*** ERROR ***" << endl << endl;
        cout << "Error: " << basic.ErrorMessage() << endl;
        cout << "Line: " << basic.ErrorLine() << endl;
        exit(1000);
    }
    exit(0);
}

The include files displayed in the listing above are mandatory to any C++ application written to embed the BASIC interpreter, as well as the declaration for the std namespace. New include files are allowed but do not remove any of those displayed.

The application above could be compiled using Code::Blocks or in a Linux terminal using g++. The compiler must be able to find the BASIC interpreter files as well as the application main file.

Assuming the file above was named test1.cpp and it's located in the same folder the with the interpreter files, the application could be compiled by typing the command line:

            g++ -Wall -std=c++11 -O3 -s basic.cpp baslib.cpp utils.cpp test1.cpp -o test1
        

This command generates the executable file test1. If we run it on a Linux terminal, the result should be the text 'hello world.' displayed just after the application call.

To compile to same application under Windows, open a command prompt, copy the application file and the BASIC interpreter files to the same folder, and type the command below under that folder:

            mingw32-g++ -Wall -std=c++11 -O3 -s basic.cpp baslib.cpp utils.cpp test1.cpp -o test1.exe
        

I'm assuming the MinGW C++ compiler is properly installed and configured.

Do not forget to comment the lines related to the _LINUX_ directive at the file common.hpp.

Note: If you are not able to compile this demo in Windows using the command prompt, use the Code::Blocks IDE and create a new empty project. It's just a matter to add the interpreter files and the test1.cpp file to the project file structure. I think this is the easiest way to use MinGW. Do not forget to enable the -std=c++11 option using the menu Build options....


Extending the BASIC interpreter

As already mentioned in this material, it's really simple to extended the interpreter by adding new functions to it. We just need to follow some few rules.

Let's take a look at the example code below.

            #include <algorithm> ///std::transform
#include <string> ///std::string
#include <iostream> ///std::cout
#include <sstream> ///std::ostringstream
#include <vector> ///std::vector
#include <map> ///std::map

using namespace std;

#include "basic.hpp"
#include "baslib.hpp"
#include "utils.hpp"

TAsmData n_times2(TAsmData *args) {
    TAsmData dt;
    dt.n = args[0].n * 2;
    return dt;
}

TAsmData s_concat(TAsmData *args) {
    TAsmData dt;
    dt.s = args[0].s + args[1].s;
    return dt;
}

///main program entry point
int main(int argc, char *argv[]) {
    CBasic basic; ///BASIC interpreter object
    TProgramFunctionsDictionary funcs; ///extension functions dictionary
    TLinkBasicFunc fnData; ///keep track of functions will be linked to the interpreter
    int res; ///Compilation result
    string bassrc(""); ///BASIC program to compile/execute

    bassrc.append("print '10 times 2 =';times2(10)\n");
    bassrc.append("print\n");
    bassrc.append("print 'concat \"hello\" and \"world\" = ';concat$('Hello','world')\n");
    bassrc.append("print\n");
    bassrc.append("print 'Ok?'\n");

    fnData.farcall = true; ///must always be "true"
    fnData.entry = &n_times2; ///"times2" entry point
    funcs["times2@n"] = fnData; ///add the new function to the dictionary
    fnData.entry = &s_concat; ///"concat$" entry point
    funcs["concat$@$$"] = fnData; ///add the new function to the dictionary

    res = basic.Compile(bassrc, funcs); ///"times2" and "concat$" can now be called from a BASIC program
    if (!res) {
        basic.ExecuteProgram();
        cout << endl;
    } else {
        cout << "*** ERROR ***" << endl << endl;
        cout << "Error: " << basic.ErrorMessage() << endl;
        cout << "Line: " << basic.ErrorLine() << endl;
        exit(1000);
    }
    exit(0);
}

The source file above is just the first C++ example with the appropriate calls to extend the BASIC interpreter, I named it "test2.cpp".

To compile it, put the test2.cpp file in a folder together with the interpreter files and call the C++ compiler:

Under Linux:

            g++ -Wall -std=c++11 -O3 -s basic.cpp baslib.cpp utils.cpp test2.cpp -o test2
        

Under Windows:

            mingw32-g++ -Wall -std=c++11 -O3 -s basic.cpp baslib.cpp utils.cpp test2.cpp -o test2.exe
        

Once more, do not forget to comment the lines related to the _LINUX_ directive if compiling for Windows.

Running the test2 application should produce the following result.

10 times 2 = 20
concat "hello" and "world" = Helloworld
Ok?

To know more about the extension methods, take a look at the baslib.cpp file. I recommend a detailed analysis at the RegisterVectorFuncs method, which integrates the BASIC interpreter with the C++ vector class.

This integration is still inefficient. It lacks error testing and more functionalities to manage vector elements, but I believe it is a good starting point to a more detailed library, and demonstrates how to use generic C++ objects with the BASIC interpreter.


Calling a BASIC function from C++

It is possible after the successfull compilation of a BASIC program, to call and execute a specific function declared in it, instead of run the entire program.

The following example shows how to call for a compiled BASIC functions using the ExecuteUserFunction method.

            #include <algorithm> ///std::transform
#include <string> ///std::string
#include <iostream> ///std::cout
#include <sstream> ///std::ostringstream
#include <vector> ///std::vector
#include <map> ///std::map

using namespace std;

#include "basic.hpp"
#include "baslib.hpp"
#include "utils.hpp"

///main program entry point
int main(int argc, char *argv[]) {
    CBasic basic; ///BASIC interpreter object
    TProgramFunctionsDictionary funcs; ///extension functions dictionary
    TLinkBasicFunc fnData; ///keep track of functions will be linked to the interpreter
    int res; ///Compilation result
    TStackType rettype; ///called function return type
    TAsmData *params, retval; ///function parameters and return value
    string bassrc(""); ///BASIC program to compile/execute

    bassrc.append("FUNCTION mulstr$(txt$, num) LOCAL i, s$\n");
    bassrc.append(" IF num <= 1 THEN RETURN txt$\n");
    bassrc.append(" s$ = ''\n");
    bassrc.append(" FOR i=1 TO num\n");
    bassrc.append(" s$ = s$ + txt$\n");
    bassrc.append(" NEXT\n");
    bassrc.append(" RETURN s$\n");
    bassrc.append("END FUNCTION\n");
    bassrc.append("\n");
    bassrc.append("PRINT 'You should not see any of these lines...'\n");
    bassrc.append("PRINT\n");
    bassrc.append("PRINT '...if running this script from the test3'\n");
    bassrc.append("PRINT\n");
    bassrc.append("PRINT 'demo program.'\n");
    bassrc.append("PRINT\n");

    res = basic.Compile(bassrc, funcs); ///no external functions needed in this example
    if (!res) {
        params = new TAsmData[2];
        params[0].s = "Yes!!!";
        params[1].n = 5;
        if (basic.ExecuteUserFunction("mulstr$@$n", params, rettype, retval))
            switch (rettype)
            {
                case TStackType(stNumber):
                    cout << "Return value (numeric): " << "'" << retval.n << "'" << endl;
                    break;
                case TStackType(stPointer):
                    cout << "Return value (pointer): " << "'" << retval.p << "'" << endl;
                    break;
                case TStackType(stString):
                    cout << "Return value (string): " << "'" << retval.s << "'" << endl;
                    break;
            }
        delete[] params;
        cout << endl;
    } else {
        cout << "*** ERROR ***" << endl << endl;
        cout << "Error: " << basic.ErrorMessage() << endl;
        cout << "Line: " << basic.ErrorLine() << endl;
        exit(1000);
    }
    exit(0);
}

This example is named test3.cpp in the interpreter folders structure.

Compile it using the same techniques described before, but updating the main file name.

Note: The result for the function call is of type string, we should see Return value (string): Yes!!!Yes!!!Yes!!!Yes!!!Yes!!! as the output for the execution of the test3 application.


Examples

The project file structure in Github includes the source code for all the three examples used above, also there are scripts to build the examples using the Linux terminal.

There is a file named editor.cpp which is the main file for an application to integrate a simple line editor to the interpreter. This editor allows the creation of new BASIC programs, loading and writing programs from/to the disk, do some very simple edition in BASIC source code, and compile/execute BASIC programs from the editor environment.

The Code::Blocks project files for the editor are also included.

Credits

This project is based in a Delphi library named DBasic, written by Dr. Fabio Cavicchio from MSB Software. Despite the porting to C++ and some enhancements, it's still strongly based on the Delphi original code.

Dr. Cavicchio kindly allowed the publication of this material.

Some final words...

I'm not (as probably you already realized) a native english speaker. I tried to revise as much as possible of this published material, but this work was made in great part during my spare time. If you find any mistake(s) that could compromise the understanding about what is being explained here, please just send an e-mail detailing what is wrong and I'll gladly fix it.

If you have any suggestion for improvements or comments about how to make this project more useful. Let me know.

My e-mail address is:

murta@andremurta.com

I'm sure, within possible, I can find some time to exchange some ideas and update my material.

Thanks for reading.