Lambda is a feature included with AWS that allows users to write a function that runs without an EC2 instance. You can write these functions in many different languages, and AWS provides the "environment" in which they'll run. CLIPS is written in C, so in order to use CLIPS in an AWS Lambda, we must use the C++ Lambda Runtime. This article will step you through creating an AWS Lambda function that can run your C(++) code.
First, go to the Lambda section of your AWS Console. Then, click the "Create function" button. From here, provide a name for your function, and select "Provide your own bootstrap on Amazon Linux 2" as your Runtime. Then, click the "Create function" button at the bottom of this page.
Below the "Function overview" section, you should see that the "Code" tab is selected. Click "Upload from" dropdown menu, then select ".zip file." The compilation process will create a zip file that we'll upload here.
Ok, now to the fun part: writing the code.
I'll reiterate the steps listed in the
C++ Lambda Runtime.
to install the AWS C++ SDK. You'll want to clone the repo from GitHub,
compile it, then install it. You'll need cmake
, git
,
make
, and the curl
development headers
on the computer you're compiling the SDK on. You'll run these
commands from the command line:
$ cd ~ $ git clone https://github.com/awslabs/aws-lambda-cpp.git $ cd aws-lambda-cpp $ mkdir build $ cd build $ cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ -DCMAKE_INSTALL_PREFIX=~/out $ make && make install
Now make a directory where you'll write your C++ lambda
and cd
into it:
$ mkdir ~/my-lambda $ cd ~/my-lambda
Download, extract, and compile the CLIPS source code:
$ wget https://sourceforge.net/projects/clipsrules/files/CLIPS/6.4.1/clips_core_source_641.tar.gz # ... truncated output ... HTTP request sent, awaiting response... 302 Found Location: https://master.dl.sourceforge.net/project/clipsrules/CLIPS/6.4.1/clips_core_source_641.tar.gz?viasf=1 [following] --2023-08-29 11:48:17-- https://master.dl.sourceforge.net/project/clipsrules/CLIPS/6.4.1/clips_core_source_641.tar.gz?viasf=1 Resolving master.dl.sourceforge.net (master.dl.sourceforge.net)... 216.105.38.12 Connecting to master.dl.sourceforge.net (master.dl.sourceforge.net)|216.105.38.12|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 1081836 (1.0M) [application/x-gzip] Saving to: ‘clips_core_source_641.tar.gz’ clips_core_source_641.tar.gz 100%[====================================================================================================>] 1.03M 1.06MB/s in 1.0s 2023-08-29 11:48:24 (1.06 MB/s) - ‘clips_core_source_641.tar.gz’ saved [1081836/1081836] $ tar --strip-components=2 -xvf clips_core_source_641.tar.gz # ... truncated output ... $ make release_cpp # ... truncated output ...
You should now have a libclips.a
file in your current directory.
This is the file we'll use to bring CLIPS into our C++ lambda.
Make a CMakeLists.txt
file with the following code in it:
cmake_minimum_required(VERSION 3.5) set(CMAKE_CXX_STANDARD 11) project(my-lambda LANGUAGES CXX) find_package(aws-lambda-runtime REQUIRED) add_executable(${PROJECT_NAME} "main.cpp") target_link_libraries(${PROJECT_NAME} PUBLIC ${CMAKE_SOURCE_DIR}/libclips.a) target_link_libraries(${PROJECT_NAME} PUBLIC AWS::aws-lambda-runtime) aws_lambda_package_target(${PROJECT_NAME})
Our main.cpp
file will use the C++ code described in the
official AWS documentation, but we'll also bring in CLIPS:
#include <aws/lambda-runtime/runtime.h> #include "clips.h" using namespace aws::lambda_runtime; invocation_response my_handler(invocation_request const& request) { Environment *mainEnv = CreateEnvironment(); BatchStar(mainEnv, "program.bat"); Run(mainEnv, -1); DestroyEnvironment(mainEnv); return invocation_response::success("ok", "text/plain"); } int main() { run_handler(my_handler); return 0; }
Our program relies upon loading a program.bat
file. Create that file
in the same directory as your main.cpp
file so that it looks like
this:
(println "Hello from CLIPS!")
In order to include this in the .zip
file that'll be created
when we compile our program, we'll need to edit the
~/out/lib/aws-lambda-runtime/cmake/packager
file slightly.
Scroll towards the bottom of this file, then add a single line:
chmod +x "$PKG_DIR/bootstrap" # some shenanigans to create the right layout in the zip file without extraneous directories pushd "$PKG_DIR" > /dev/null cp ../../program.bat . zip --symlinks --recurse-paths "$PKG_BIN_FILENAME".zip -- * ORIGIN_DIR=$(dirs -l +1) mv "$PKG_BIN_FILENAME".zip "$ORIGIN_DIR" popd > /dev/null rm -r "$PKG_DIR" echo Created "$ORIGIN_DIR/$PKG_BIN_FILENAME".zip
Now we'll compile it and create our .zip
file:
$ mkdir build $ cd build $ cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=~/out $ make $ make aws-lambda-package-my-lambda
You should now have a my-lambda.zip
file in your current directory.
Upload this file via the AWS console as detailed in the
Creating the Lambda section of this article.
After we've uploaded our .zip
file, we can finally test
things out. Navigating to the "Test" tab under the "Function overview"
section presents a "Test" button. Click this button to run the test.
You should see a success message. If you open this message, you'll see
that our lambda returned "ok." Clicking the "logs" link in that same
message window sends us to the CloudWatch logs for this lambda.
Click the first link in the table presented on this screen to see the output of our CLIPS program.
There's the output of our CLIPS program! You can see the text we printed
out using println
in our program.bat
file
on the third log line. Now we'll talk about how to
read the http request and write the response body from within CLIPS.
We can add different I/O methods to our CLIPS program by adding a
"Router." I discussed what a CLIPS Router is and how to do this in
a previous article, so we'll move right on
to implementation. Update your main.cpp
file
so that it has two new Routers:
one for reading the request, the other for writing the response.
#include <string> #include <sstream> #include <aws/lambda-runtime/runtime.h> #include "clips.h" using namespace aws::lambda_runtime; bool QueryOutCallback(Environment *env, const char *logicalName, void *_) { return strcmp(logicalName, "lambda-out") == 0; } void WriteOutCallback(Environment *env, const char *logicalName, const char *str, void *context) { std::stringstream *ss = (std::stringstream*)context; *ss << str; } bool QueryInCallback(Environment *env, const char *logicalName, void *_) { return strcmp(logicalName, "lambda-in") == 0; } int ReadInCallback(Environment *env, const char *logicalName, void *context) { invocation_request *request = (invocation_request*)context; char toReturn = '\n'; if(!request->payload.empty()) { toReturn = request->payload.front(); request->payload.erase(request->payload.begin()); } return toReturn; } int UnreadInCallback(Environment *env, const char *logicalName, int c, void *context) { char c_char = (char)c; invocation_request *request = (invocation_request*)context; request->payload = c_char + request->payload; return c; } invocation_response my_handler(invocation_request const& request) { Environment *mainEnv = CreateEnvironment(); std::stringstream ss; AddRouter( mainEnv, "lambda-out", 20, QueryOutCallback, WriteOutCallback, NULL, NULL, NULL, &ss); AddRouter( mainEnv, "lambda-in", 20, QueryInCallback, NULL, ReadInCallback, UnreadInCallback, NULL, const_cast<void *>((const void *)(&request))); BatchStar(mainEnv, "program.bat"); Run(mainEnv, -1); DestroyEnvironment(mainEnv); //return invocation_response::success("ok", "text/plain"); return invocation_response::success(ss.str(), "text/plain"); } int main() { run_handler(my_handler); return 0; }
QueryOutCallback
and QueryInCallback
are simple: if the Router CLIPS attempts to printout
to or readln
from is called lambda-out
or lambda-in
respectively, we'll use this Router's
callback functions. WriteOutCallback
is how
we'll write our http response. Its last argument context
is a void pointer. We cast it to a std::stringstream
so we can use it as a buffer. We'll eventually write the contents
of this std::stringstream
to the response body of our
lambda function.
ReadInCallback
also receives a context
void pointer. This is the request as sent in to the lambda handler,
so we can cast it as invocation_request
. We'll read
the payload character by character. An invocation_request
has a property payload
which is a std::string
,
so we can use empty
to make sure it's not empty before
"shifting" the first character from the string with
front
, erase
, and begin
.
UnreadInCallback
should do the opposite: it puts the
second const char
argument back on to the front of the
string.
In my_handler
, we add two calls to AddRouter
to add our new Routers. Note the funky casting we have to do
for the context
argument of the lambda-in
Router function. That's because the invocation_request
is a const
; we must first cast to a const void
pointer before casting to a void
pointer.
Finally, we replace the hard coded "ok"
with ss.str()
in our invocation_response::success
call.
This will return the contents of our std::stringstream
buffer
we wrote to in WriteOutCallback
.
One final touch: we'll replace the contents of our
program.bat
file so that it uses these two new routers:
(printout lambda-out "You sent: " (readline lambda-in))
This is effectively an echo server demonstrating how to get the request
and write the response from within your CLIPS program. From inside of your
build
directory, follow the previous
compilation steps to generate a new .zip
file:
$ make clean $ make $ make aws-lambda-package-my-lambda
Upload the new .zip
file as described in our previous steps.
Now we'll click that "Test" button:
Voila!
In this article, we demonstrated how to use CLIPS from within an AWS lambda. We learned how to add Routers to CLIPS so that our Rules Engine could read the http request made to the lambda as well as write to the http response of the lambda. I hope this was helpful for you!
- ryjo