Progmatism.com

EclGui



Abstract

EclGui is a small project uniting Embeddable Common Lisp with a Visual Basic frontend, essentially providing a win32 gui for an ansi compliant lisp interpreter. It's purposes include:

  • To demonstrate for beginners how to embed ECL in applications.
  • To provide a compelling example of how to distribute lisp binaries in Windows.
  • To make something neat!

You can get the completed binary here: Download

An in-depth guide to the code follows, and you can get the source at the bottom of the page.

ECL

If you intend on building EclGui yourself, you'll first need to get the source for ECL and compile it. Doing so is not too difficult, there are excellent directions on the ECL page.

When built, ECL provides a single dll (on Windows) or shared library (on GNU/Linux), with an exported symbol corresponding to each symbol in standard common lisp, as well as some for ECL extensions. What this means is that you can treat the embedded library as a lisp environment, constructing lisp data structures and calling lisp functions directly from C.

There are some issues with doing so, however. For one, no lisp stack is constructed when directly calling lisp's exported symbols. Errors will cause a crash. If your program needs to run unknown code, it's best to use the ECL extension si_safe_eval. This functions evaluates lisp code, but handles errors elegantly; it is the key to the EclGui program. We will use it to run code inputted by the user, but in other cases construct code directly which is more efficient.

It's also sometimes necessary to callback from lisp code into C. This is used in EclGui to implement gray streams that capture what ECL thinks is console output.

C++ Wrapper

For some applications, being able to embed lisp is compelling enough. However, in many cases it is necessary to translate data to and from the lisp world to ease the integration with whatever environment you're using. That is certainly the case in our example. Visual Basic uses its own string data type, links to its own subsystem, and in our case needs to evaluate user inputted code. Fortunately, this provides the perfect opportunity to demonstrate how to wrap ECL.

A small amount of C++ code in EclGui provides a glue dll to wrap ECL, and perform the needed data conversions. The two entry points for this C++ code are glueInit, which must be called once at initialization to prepare the lisp world, and glueCall, which takes lisp code as a string, and returns a string of the evaluated result.

int _stdcall glueInit(PSCB funcptr)
{
    int argc = 1;
    char * argv[1];
    argv[0] = "";
    cl_boot(argc, argv);
    disableConsole();
    termIOInit(funcptr);
    globalsInit();
    return 0;
}

Here is the code for glueInit. It first calls cl_boot, a function in ECL which must be called before the embedded lisp is used. Because we're using Windows, we also need to account for the fact that Windows uses a different subsystem than ECL expects. If ECL tries to write to the console, but our gui app is bound to the Windows subsystem, it will cause a crash. So the call to disableConsole disables console output. In the same vein, termIOInit redirects the console output back to the VB layer, displaying it in a message box. The function parameter funcptr holds a pointer to a function that performs actual output display. Finally, globalsInit readies some custom symbols we use for error handling.

BSTR _stdcall glueCall(LPCSTR inStr)
{
    cl_object clObj = eclRun(inStr, false);
    const char * errorMessage;
    if (isError(clObj, &errorMessage))
    {
        MessageBox(NULL, errorMessage, "ERROR", 0);
        return A2BSTR("");
    }
    std::string strView = eclView(clObj);
    BSTR bstr = A2BSTR(strView.c_str());
    return bstr;
}

Here is the code for glueCall. It starts by calling eclRun. This is the function that gets called every time some lisp code needs to be evaluated. The first parameter is the code as a string, and the second parameter is a boolean for whether the code is trusted and the unsafe reader can be used. Since we're running user generated code, we always want this argument to be 'false'.

The next few lines handle any errors that might occur, such as errors in the reader, or undefined variables or functions. At the end is a call to eclView, which converts a lisp value back into a string, then this is turned into a string VB can use.

cl_object eclRun(std::string strCmd, bool useUnsafeReader)
{
    cl_object form, result;
    if (useUnsafeReader)
    {
        form = c_string_to_object(strCmd.c_str());
    }
    else
    {
        form = safeRead(strCmd);
        if (isError(form, NULL))
        {
            return form;
        }
    }
    cl_object result =
        si_safe_eval(3, form, Cnil, g_evalErrorSymbol);
    return result;
}

The function eclRun demonstrates how to interact directly with ECL. c_string_to_object is like Common Lisp's read-from-string except that it works directly with c-strings. si_safe_eval is a ECL extension which runs the code in a lisp form and guarantees that no errors will be thrown. The fourth parameter g_evalErrorSymbol is a symbol that gets returned only in the case that si_safe_eval catches an error instead of running successfully.

cl_object safeRead(const std::string & strCmd)
{
    cl_object command =
        make_simple_base_string((char*)strCmd.c_str());
    cl_object readFromStringSymbol =
        c_string_to_object("READ-FROM-STRING");
    cl_object form = CONS(readFromStringSymbol,
                          CONS(command, Cnil));
    cl_object read =
        si_safe_eval(3, form, Cnil, g_readErrorSymbol);
    return read;
}

As seen in eclRun, sometimes we don't know if code is safe to read. It may have unbalanced parenthesis, or invalid tokens. In order to prevent errors from crashing our program, we use safeRead which will use si_safe_eval to read our code string. We manually build a lisp form using CONS, such that it generates the structure needed for a runnable lisp form. If an error occurs while reading, then g_readErrorSymbol will be returned, otherwise the return value will be the string that got read, structured as a lisp value.

One disadvantage of this code is that if an error occurs some information is dropped, such as the error report. An alternate approach is to build a typical Common Lisp handler form, such as handler-bind. Inside you would capture the error and return a tuple from your lisp code, in the form (<successful return value> <error value>). This approach has been omitted for simplicity's sake.

void termIOInit(PSCB funcptr)
{
    g_termIOSink = funcptr;
    cl_def_c_function(
        cl_intern(1, make_simple_base_string(
                         SYMBOL_WRITE)),
        (void *)termIOWriteChar, 1);
    cl_def_c_function(
        cl_intern(1, make_simple_base_string(
                         SYMBOL_FLUSH)),
        (void *)termIOFlush, 0);
    eclRun("(defclass %term-io% "
           "  (gray:fundamental-character-output-stream)"
           "    (last-char))", true);
    ...
}

Now let's skip ahead to the end of the source, to the last interesting function. termIOInit prepares the console output to get returned back to the VB layer. It uses ECL's ffi mechanism to allow lisp code to call back into c code. cl_def_c_function takes three parameters, the first being the lisp symbol that names the callback, the second is a function pointer to the c callback, and the third is the number of arguments that the callback takes. ECL hooks up the appropriate parts so that the lisp symbol may be called to invoke the c function.

These functions allow us to implement gray streams. The next line is a lisp form that starts the definition of the gray stream class. Gray streams act as a custom stream type, which in this case will substitute the normal console output with a Windows message box. The call to eclRun uses the unsafe reader, since we're calling our own lisp code, which we know is syntactically valid.

Visual Basic

The VB code, by comparison, is very simple. VB does not have a very strong ffi, but it's enough to get by. For one, both of our C++ exports had to be declared to use the stdcall convention, so that VB could call them correctly.

Private Declare Function glueInit _
                Lib "glue.dll" (ByVal pf As Any) _
                As Integer
Private Declare Function glueCall _
                Lib "glue.dll" (ByVal inStr As String) _
                As String

These are the foreign functions as declared by our VB code. Nothing too surprising here.

Private Sub Form_Load()
    nRet = glueInit(AddressOf Callback_Output)
End Sub

Private Sub m_btnExec_Click()
    strCode = m_txtInput.Text
    strOutc = glueCall(strCode)
    strRet = StrConv(strOutc, vbFromUnicode)
    m_txtOutput.Text = strRet
End Sub

As noted above, we need to call glueInit to initialize our environment. VB gives us the Form_Load handler to prepare our app, so its the perfect place for this call. Next, we note the main star of the front end, the m_btnExec_Click function. This is triggered by a click of the Execute button, and simply calls the glueCall function to evaluate the lisp code that's in the textbox.

Public Sub Callback_Output(Str As String)
    nRet = MsgBox(Str, vbOKOnly, "Output")
End Sub

When the VB code calls glueInit, it passes the address of this Callback_Output function. Due to a strange quirk of VB, any function passed by address needs to be in an external file. The purpose of this callback is to handle redirected console output, in this case displaying it in a message box.

That basically sums up the key parts of EclGui. There are a few more details to tie it all together; I'd encourage you to play around with the source if you're interested. It is fairly self-explanatory and contains comments for the parts that may not be crystal clear.

For more information on ECL, have a look at its thorough user manual.

Downloads

Binary
Just extract and run, no need to install.

Source code
To compile, you'll need Microsoft Visual C++ and Visual Basic. The C++ dll comes with a simple batch script, and a msvc project file.