DragonFly On-Line Manual Pages
KLELTUT(1) libklel KLELTUT(1)
INTRODUCTION
This manual page provides a tutorial for the creation of a simple
application that embeds the KL-EL compiler and interpreter.
BASIC EXAMPLE
Here is a simple application that uses KL-EL, in its entirety. Note
that the line numbers are for explanatory purposes.
1 #include <klel.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4
5 int
6 main(int iArgumentCount, char **ppcArgumentVector)
7 {
8 KLEL_CONTEXT *psContext = NULL;
9 KLEL_VALUE *psResult = NULL;
10 char *pcExpression = (iArgumentCount >= 2) ? ppcArgumentVector[1] : "";
11 char *pcMessage = NULL;
12 size_t szLength = 0;
13 psContext = KlelCompile(pcExpression, 0, NULL, NULL, NULL);
14 if (KlelIsValid(psContext))
15 {
16 psResult = KlelExecute(psContext);
17 if (psResult != NULL)
18 {
19 pcMessage = KlelValueToString(psResult, &szLength);
20 fprintf(stdout, "result: %s\n", pcMessage);
21 KlelFreeResult(psResult);
22 free(pcMessage);
23 }
24 else
25 {
26 fprintf(stderr, "error: %s\n", KlelGetError(psContext));
27 }
28 }
29 else
30 {
31 fprintf(stderr, "error: %s\n", KlelGetError(psContext));
32 }
33 KlelFreeContext(psContext);
34
35 return 0;
36 }
This example includes basic error handling and shows how simple
embedding KL-EL can be. When compiled and linked with the KL-EL
library, this program will take the first argument (which represents a
KL-EL expression) and compile/execute it. Then, the result of that
execution is converted to a string value and printed to stdout.
Let's analyze what's going on here. The first really interesting call
is this:
13 psContext = KlelCompile(pcExpression, 0, NULL, NULL, NULL);
This compiles the expression (or an empty string if the user didn't
provide an argument) and returns a KLEL_CONTEXT structure. The 0 means
that we aren't passing any special flags to the compiler. The next two
NULLs mean that we're not exporting any variables into the KL-EL
envrionment, and the final NULL means that we have no user-defined data
to pass around.
Next we check to make sure the compile was successful:
14 if (KlelIsValid(psContext))
If KlelIsValid returns zero (false), it means that an error occurred
during compilation and the resulting context, if any, is useless for
anything except getting error messages. Assuming compilation
succeeded, we then proceed to execute the expression:
16 psResult = KlelExecute(psContext);
KlelExecute will return NULL if execution fails with any generated
error messages being stored in the provided context. If execution
doesn't fail, we proceed to print out the result of the execution and
free the result since we're done using it:
19 pcMessage = KlelValueToString(psResult, &szLength);
20 fprintf(stdout, "result: %s\n", pcMessage);
21 KlelFreeResult(psResult);
22 free(pcMessage);
Most of the rest of the example is simply error handling that is called
if either compilation or execution fails.
The final thing we do is free the context for the expression:
33 KlelFreeContext(psContext);
And that's it!
Note that the context could have been executed again or as many times
as you wish. There's no global state in KL-EL, so different threads
can compile expressions and run them simultaneously (though a single
context shouldn't be shared across threads without proper handling and
serialization).
Assuming the source file from above is named "klel-basic.c" and you
have an appropriate build environment (including gcc, libklel, and
libpcre), you should be able to compile the code as follows:
$ gcc -o klel-basic klel-basic.c -lklel -lpcre
Try out a few simple expressions:
$ klel-basic '2 + 2'
result: 4
$ klel-basic 'pi / e'
result: 1.15573
$ klel-basic '"0x" . hex_of_int(65536)'
result: 0x10000
This tutorial includes a few more complicated examples below, but this
example shows the basic workflow followed by any application that
embeds KL-EL.
EXPORTING FUNCTIONS AND VARIABLES EXAMPLE
Here is some source code that we can add to the example program above
that demonstrates how to export variables and functions into the KL-EL
environment where they can be used in KL-EL expressions. We export an
integer variable, "arg_count", that holds the number of command line
arguments passed to the application. We also export a function,
"get_arg", that returns a string representation of the numbered command
line argument.
The code below should be inserted into the example above starting at
line 4.
1 #include <string.h>
2
3 int giArgumentCount = 0;
4 char **gppcArgumentVector = NULL;
5
6 KLEL_VALUE *
7 GetArg(KLEL_VALUE **ppsArgs, void *pvContext)
8 {
9 int64_t i64Arg = ppsArgs[0]->llInteger;
10
11 if (i64Arg < 0 || i64Arg >= giArgumentCount)
12 {
13 KlelReportError((KLEL_CONTEXT *)pvContext, "get_arg: invalid argument", NULL);
14 return NULL;
15 }
16
17 return KlelCreateString(strlen(gppcArgumentVector[i64Arg]), gppcArgumentVector[i64Arg]);
18 }
19
20 KLEL_EXPR_TYPE
21 GetType(const char *pcName, void *pvContext)
22 {
23 if (strcmp(pcName, "arg_count") == 0)
24 {
25 return KLEL_TYPE_INT64;
26 }
27 else if (strcmp(pcName, "get_arg") == 0)
28 {
29 return KLEL_TYPE_STRING_FUNCTION1(KLEL_TYPE_INT64);
30 }
31
32 return KLEL_TYPE_UNKNOWN;
33 }
34
35 KLEL_VALUE *
36 GetValue(const char *pcName, void *pvContext)
37 {
38 if (strcmp(pcName, "arg_count") == 0)
39 {
40 return KlelCreateInteger(giArgumentCount);
41 }
42 else if (strcmp(pcName, "get_arg") == 0)
43 {
44 return KlelCreateFunction(KLEL_TYPE_STRING_FUNCTION1(KLEL_TYPE_INT64), "get_arg", GetArg);
45 }
46
47 return KlelCreateUnknown();
48 }
After inserting this code into the basic example above, change the
basic example's compile expression from this
13 psContext = KlelCompile(pcExpression, 0, NULL, NULL, NULL);
to this
13 psContext = KlelCompile(pcExpression, 0, GetType, GetValue, NULL);
14 giArgumentCount = iArgumentCount;
15 gppcArgumentVector = ppcArgumentVector;
By doing this, KL-EL expressions now have access to a new variable,
"arg_count" and a new function "get_arg". Let's examine the new code
in depth.
KL-EL allows you to export variables and functions by defining two
callback functions. One function is called with a variable name and
returns the type of that variable. The other function is called with a
variable name and returns the value of that variable. The type of a
variable can never change, but a variable's value can change at any
time. We export the GetArg function as "get_arg" and the
giArgumentCount variable as "arg_count".
Let's start by looking at the type callback, GetType:
20 KLEL_EXPR_TYPE
21 GetType(const char *pcName, void *pvContext)
22 {
23 if (strcmp(pcName, "arg_count") == 0)
24 {
25 return KLEL_TYPE_INT64;
26 }
27 else if (strcmp(pcName, "get_arg") == 0)
28 {
29 return KLEL_TYPE_STRING_FUNCTION1(KLEL_TYPE_INT64);
30 }
31
32 return KLEL_TYPE_UNKNOWN;
33 }
This function simply checks the name of the variable it's supposed to
look up and returns the appropriate type: KLEL_TYPE_INT64 for
"arg_count" and a slightly more complicated type descriptor for
"get_arg" that says that it is a function that returns a string and
takes one argument, an integer. If it's passed a variable name it
doesn't know, it returns KLEL_TYPE_UNKNOWN, which causes KL-EL to
search the standard library to see if it can be found there.
In this simple example we just use a cascading if-then-else to figure
out which variable name was passed in. In larger applications, a hash
function might be used to hash the variable name and turn it into a
table lookup.
Now let's look at the function that returns those variables' values,
GetValue:
35 KLEL_VALUE *
36 GetValue(const char *pcName, void *pvContext)
37 {
38 if (strcmp(pcName, "arg_count") == 0)
39 {
40 return KlelCreateInteger(giArgumentCount);
41 }
42 else if (strcmp(pcName, "get_arg") == 0)
43 {
44 return KlelCreateFunction(KLEL_TYPE_STRING_FUNCTION1(KLEL_TYPE_INT64), "get_arg", GetArg);
45 }
46
47 return KlelCreateUnknown();
48 }
The basic structure of this function is similar to GetType: it
determines which variable is being requested, and returns the
appropriate value. It uses the KlelCreateInteger function to create
the integer value for "arg_count", and KlelCreateFunction to create the
function value for "get_arg". By returning the result of
KlelCreateUnknown if it doesn't know the value of the variable, it
causes KL-EL to search the standard library.
If the values of the exported variables were calculated dynamically
(which they certainly can be), returning NULL instead of the result of
KlelCreateUnknown would signal to KL-EL that the variable is known but
an error occurred. In that case, KL-EL won't search the standard
library.
Both GetType and GetValue take a second argument, a void pointer to the
context. This is the same value (cast as a void pointer) that was
returned by KlelCompile.
The value returned for "arg_count" isn't that interesting -- it's just
an integer. The value returned for "get_arg" is much more interesting
-- it's a function that will get invoked by the library. Let's look at
the exported function, GetArg:
6 KLEL_VALUE *
7 GetArg(KLEL_VALUE **ppsArgs, void *pvContext)
8 {
9 int64_t i64Arg = ppsArgs[0]->llInteger;
10
11 if (i64Arg < 0 || i64Arg >= giArgumentCount)
12 {
13 KlelReportError((KLEL_CONTEXT *)pvContext, "get_arg: invalid argument", NULL);
14 return NULL;
15 }
16
17 return KlelCreateString(strlen(gppcArgumentVector[i64Arg]), gppcArgumentVector[i64Arg]);
18 }
Just like GetValue, exported functions must return a pointer to a
KLEL_VALUE. Exported functions take two arguments, an array of
pointers to KLEL_VALUE representing the the function's arguments, and a
void pointer for the current context. There are always exactly
thirteen entries in ppsArgs, though they aren't all necessarily filled
in. The zeroeth entry is the function's first argument, and arguments
are specified in ascending order.
GetArg is pretty simple -- it just takes the value of its first
argument (an integer) and either returns the corresponding command line
argument as a string, or it reports an error and returns NULL. The
library's internal type checker guaranteed that this function would be
called with one integer argument, so it's technically okay that this
example doesn't explicitly check -- but there's no harm in double
checking if you want.
Finally, the last change needed to export our variable and function was
to pass the two callback functions to KlelCompile. Once that is done,
your new variable and function are ready for use in KL-EL expressions.
GUARDED COMMANDS
What Are Guarded Commands?
Guarded commands are a kind of two-part expression. The first part of
the expression is called the "guard", and the second part is the
"command". The guard must be a boolean expression, and the command
must be a call to the special function "eval".
Guarded commands allow for extra information to be passed to an
application from the expression. The extra information consists of two
strings, known as the "interpreter" and the "program", a collection of
256 integers called "exit codes", and up to twelve additional strings.
Conventionally, guarded commands are used to pass operating system
commands to the application to be executed, but KL-EL assigns no
semantics to them other than that they are guarded commands with a
boolean expression for a guard. KL-EL has been used as a "control
language" for some applications that embed multiple programming
language interpreters (Python, Perl, Lua, etc) to determine which
interpreter should be run on a given set of input by using guarded
commands.
Guarded commands look like this:
if (filename == "/etc/passwd") then
eval("exec", "/bin/rm", "-f", filename) pass [0, 255]
The part in parentheses after the "if" is the guard. The rest of the
expression after the "then" is the command. In this example, "exec" is
the interpreter and "/bin/rm" is the program.
One might read this example as saying "if the value of the 'filename'
variable is '/etc/passwd', then execute the '/bin/rm' command with the
following arguments and assume the operation was successful if the exit
code is 0 or 255.
While you could read it that way, KL-EL doesn't enforce any of that --
it's up to your application to decide what to do with the success
criteria provided in this expression. KL-EL provides functions to
extract each of the various parts of the guarded command.
There are some type checking and syntactic limits enforced by KL-EL on
guarded commands:
The guard expression must be boolean
The first two arguments to the eval function must be literal strings
with no interpolations and shorter than 255 bytes.
Exit codes must be literal integers.
Compiling and Executing Guarded Commands
If you just use KlelCompile and KlelExecute and don't run the guarded
command in the specified interpreter, then KL-EL will simply treat a
guarded command as a boolean expression. More specifically, it will
act as though the guard is the whole expression.
The function KlelIsGuardedCommand will test to see if a compiled
expression is a guarded command. If you want to force the user to only
provide guarded commands, the KLEL_MUST_BE_GUARDED_COMMAND flag should
be passed to KlelCompile. If this flag is set, KlelCompile will return
a compilation error for any requests to compile a non-guarded command.
If a guarded command is supplied, then you can use the following
functions on the compiled expression:
KlelGetInterpreter
returns the interpreter argument to the eval function
KlelGetProgram
returns the program argument to the eval function
KlelIsSuccessReturnCode
returns true or false if the code passed in is a successful code
according to the expression.
None of the functions above will cause evaluation of the remaining
arguments to the eval function, so they can be called even before your
callbacks are ready to be executed. This allows you to make sure these
arguments are valid and set up any external interpreters you may need.
Once you're ready to evaluate the remaining arguments to the eval
function, you can call the KlelGetCommand function. Presumably you'd
do this if the result of KlelExecute was true, but it's up to you. The
KlelGetCommand function returns a pointer to a KLEL_COMMAND structure.
This structure contains all of the information about the guarded
command and is defined as follows:
typedef struct _KLEL_COMMAND
{
char pcInterpreter[KLEL_MAX_NAME + 1];
char pcProgram[KLEL_MAX_NAME + 1];
size_t szArgumentCount;
char *ppcArgumentVector[KLEL_MAX_FUNC_ARGS + 1];
int aiCodes[256];
} KLEL_COMMAND;
The pcInterpreter and pcProgram arguments simply contain the associated
string. The aiCodes array contains a nonzero value in every position
that corresponds to a successful exit code. Note that if the
expression does not contain any exit codes (they are optional), then 0
is considered the only successful exit code.
Most interesting, however, is the ppcArgumentVector array. This
contains the values of the remaining arguments to the eval function
converted to strings. As a convenience, the first entry in this array
is identical to the contents of the pcProgram member; this makes it
easy to interface with the execv(3) family of functions.
Once you're done with a KLEL_COMMAND structure, it needs to be freed
using KlelFreeCommand.
The klel-expr program included in the KL-EL source distribution accepts
normal expressions and guarded commands. It evaluates normal
expressions and prints their results, but for guarded commands, it
defines two interpreters: "echo" and "system". For the "echo"
interpreter, it simply echos the contents of ppcArgumentVector. The
"system" interpreter takes the contents of pcProgram and passes it to
the standard system(3) function.
SEE ALSO
klel-expr(1), klelapi(3), klellang(3), klelstdlib(3)
1.1.0 2015-09-16 KLELTUT(1)