This article is the next part of Brainfuck interpreter implementation. We built the library that parses and executes Brainfuck language in the previous part. In this part, we will use this library in the CLI program and deploy the code to AWS Lambda. Let’s begin with the CLI.
CLI program implementaiton
Firstly, we need to create a new application in our workspace. In the terminal, go to the root workspace directory and run this command:
cargo new brainfuck_cli
Then, add a new application to the root Cargo.toml
[workspace]
members = [
"brainfuck_interpreter",
"brainfuck_cli"
]
Dependency definition
I highly recommend using the clap library for building CLI applications. We can also use anyhow to interact with any error type easier. You should use thiserror for libraries and anyhow for applications as a rule of thumb.
The last line in the dependencies uses our brainfuck_interpreter library in the same workspace.
[dependencies]
clap = { version = "3.1.5", features = ["derive"] }
anyhow = "1.0"
brainfuck_interpreter = { path = "../brainfuck_interpreter" }
Application code
CLI arguments definition
clap crate allows us to define needed arguments by structure. Here how it looks likes:
use clap::Parser;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
source_path: String,
}
Our program needs the path to the source. We will read the file’s content and execute it on the fly.
To read arguments in the main method, we should do this:
use anyhow::Result;
fn main() -> Result<()> {
let args = Args::parse();
// Other code
}
Note, we use anyhow::Result as the returned value from main. It will handle any error without the need to write different handlers for each type of error.
Main application code
To read the file’s content, we can use the standard library method fs::read_to_string(args.source_path). We need to use io::stdin as input: io::Read argument to interpret method. After that, we can write all the output to stdout.
fn main() -> Result<()> {
let args = Args::parse();
let source = fs::read_to_string(args.source_path)?;
let stdin = io::stdin();
let result = interpret(&source, Box::new(stdin))?;
io::stdout().write_all(result.as_bytes())?;
Ok(())
}
CLI Testing
Let’s execute the program:
cargo run
You will receive this output:
error: The following required arguments were not provided:
<SOURCE_PATH>
USAGE:
brainfuck_cli <SOURCE_PATH>
For more information try --help
Let’s see the help page:
cargo run -- --help
Output:
brainfuck_cli 0.1.0
USAGE:
brainfuck_cli <SOURCE_PATH>
ARGS:
<SOURCE_PATH>
OPTIONS:
-h, --help Print help information
-V, --version Print version information
Note how descriptive and easy to use our application is. clap library provided best practices of CLI software development for free.
To run a sample Hello world application located at brainfuck_examples/hello.bf, you should execute this command:
cargo run -- ../brainfuck_examples/hello.bf
Hello World!
A more sophisticated example, cat.bf, will output everything from the input. You should start the program, type anything and then press CTRL + D in Linux/Mac or CTRL + Z in Windows to finish stdin:
cargo run -- ../brainfuck_examples/cat.bf
I love rust[CTRL+D]
I love rust
You can also redirect stdin from other files:
echo "blah blah" | cargo run -- ../brainfuck_examples/cat.bf
blah blah
AWS Lambda program implementaiton
This application will run inside the AWS Lambda, accept HTTP request events and return HTTP responses. To work correctly, we will configure API Gateway. It will redirect all the requests to AWS Lambda and return responses to the user.
Follow the same steps from CLI to create the brainfuck_aws application inside the workspace:
cargo new brainfuck_aws
Dependency definition
To work with API Gateway and lambda, we should use the official lambda_http crate. It has all the required structures to work with them.
We also need to serialize and deserialize all the inputs/outputs. We use serde and serde_json for that. Tokio is used to provide asynchronous I/O runtime for our application. Lastly, we use env_logger and log to provide logging functionality. The runtime will send them to Cloudwatch.
[dependencies]
brainfuck_interpreter = { path = '../brainfuck_interpreter' }
lambda_http = "0.5"
serde_json = "1.0"
serde = "1.0"
tokio = "1.0"
env_logger = "0.9"
log = "0.4"
Application code
Request and response types
Our request should contain the source code and the input:
#[derive(Deserialize)]
struct InterpreterRequest {
source: String,
input: Option<String>,
}
The response should return either the result of successful execution or an error message:
#[derive(Serialize, Debug)]
#[serde(rename_all = "lowercase")]
enum InterpreterResponse {
Success(String),
Error(String),
}
Main code
During the startup, we should enable logging and start lambda runtime:
#[tokio::main]
async fn main() -> Result<(), Error> {
env_logger::init();
info!("Starting lambda function!");
let func = service_fn(func);
lambda_http::run(func).await?;
Ok(())
}
Lambda runtime will execute our func function every time on event fire. This function should process the request. Depending on the interpreter execution result, it will return either 200 OK with the stdout, or 400 BAD REQUEST with the error description:
async fn func(event: Request) -> Result<impl IntoResponse, Error> {
debug!("Received request: {:?}", event);
info!("Processing request!");
match process_request(event).await {
Ok(result) => Ok(Response::builder()
.status(200)
.body(serde_json::to_string(&result)?)?),
Err(error) => Ok(Response::builder().status(400).body(serde_json::to_string(
&InterpreterResponse::Error(error.to_string()),
)?)?),
}
}
Before the execution, we should parse the payload. If it’s not a valid payload, we can return an error. Otherwise, we can start code parsing and execution. In case of any error’s inside the brainfuck library, we will return them; otherwise, we will return stdout:
async fn process_request(request: Request) -> Result<InterpreterResponse, Error> {
if let Some(request) = request.payload::<InterpreterRequest>()? {
debug!("Body is valid. Processing request");
let source = request.source;
let input = request.input.unwrap_or(String::new());
let stdin = Box::new(input.as_bytes());
let result = match interpret(&source, stdin) {
Ok(output) => InterpreterResponse::Success(output),
Err(error) => InterpreterResponse::Error(error.to_string()),
};
info!("Interpreter result: {:?}", result);
Ok(result)
} else {
warn!("Can't process request. Invalid body");
Err("Invalid body")?
}
}
Template definition
We will use SAM (Serverless application mode) to simplify our deployment to AWS. SAM allows specifying only the function you need and required endpoints. Under the hood, it will create API Gateway, AWS Lambda, IAM Roles, Databases, etc.
In this example, we need AWS Lambda that API Gateway will call. It will be called BrainfuckFunction, use x86_64 architecture and Amazon Linux 2 runtime. API Gateway will provide a POST /brainfuck endpoint that triggers AWS Lambda.
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
BrainfuckFunction:
Type: AWS::Serverless::Function
Properties:
MemorySize: 128
Architectures: ["x86_64"]
Handler: bootstrap
Runtime: provided.al2
Timeout: 5
CodeUri: .
Events:
Brainfuck:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /brainfuck
Method: post
Outputs:
RestApi:
Description: "API Gateway endpoint URL for Prod stage for Brainfuck function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/brainfuck/"
BrainfuckFunction:
Description: "Brainfuck World Lambda Function ARN"
Value: !GetAtt BrainfuckFunction.Arn
BrainfuckFunctionIamRole:
Description: "Implicit IAM Role created for Brainfuck World function"
Value: !GetAtt BrainfuckFunctionRole.Arn
Makefile
We need to create a makefile file to simplify our work. We can run make in the console, and it will compile our application and prepare it to be deployed to AWS Lambda.
Note that we are building an application with x86_64-unknown-linux-musl architecture. It’s because AWS Lambda requires us to use it.
The process of building is the following:
- Add
x86_64-unknown-linux-musltarget torustup. - Compile application with
muslarchitecture. sam buildwill executebuild-BrainfuckFunctioninternally.- Copy compiled binary into the output directory.
- Discards symbols from compiled binary to reduce size.
ARCH = x86_64-unknown-linux-musl
ROOT_DIR = $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
build:
rustup target add $(ARCH)
cargo build --target $(ARCH) --release --target-dir ../target
sam build
build-BrainfuckFunction:
cp $(ROOT_DIR)/../target/$(ARCH)/release/brainfuck_aws $(ARTIFACTS_DIR)/bootstrap
strip $(ARTIFACTS_DIR)/bootstrap
Deployment
AWS CLI configuration
Almost everything is done. You need to download and install AWS CLI and SAM CLI. Then you need to configure the AWS client before deployment:
aws configure
You should set up your access key id and secret access key. You should do it once. Then it will persist locally.
SAM deployment
To initiate deployment for the first time, you should run this command:
sam deploy --guided
It will prompt you about the function name, region, etc. After that, it will create a local file, samconfig.toml. The next time you can use sam deploy command.
After initial configuration, it will create all the resources and deploy our application. You should get similar output to this:
CloudFormation events from stack operations
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus ResourceType LogicalResourceId ResourceStatusReason
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS AWS::IAM::Role BrainfuckFunctionRole -
CREATE_IN_PROGRESS AWS::IAM::Role BrainfuckFunctionRole Resource creation Initiated
CREATE_COMPLETE AWS::IAM::Role BrainfuckFunctionRole -
CREATE_IN_PROGRESS AWS::Lambda::Function BrainfuckFunction -
CREATE_IN_PROGRESS AWS::Lambda::Function BrainfuckFunction Resource creation Initiated
CREATE_COMPLETE AWS::Lambda::Function BrainfuckFunction -
CREATE_IN_PROGRESS AWS::ApiGateway::RestApi ServerlessRestApi -
CREATE_IN_PROGRESS AWS::ApiGateway::RestApi ServerlessRestApi Resource creation Initiated
CREATE_COMPLETE AWS::ApiGateway::RestApi ServerlessRestApi -
CREATE_IN_PROGRESS AWS::ApiGateway::Deployment ServerlessRestApiDeployment683b01a6bf -
CREATE_IN_PROGRESS AWS::Lambda::Permission BrainfuckFunctionBrainfuckPermissionPr -
od
CREATE_IN_PROGRESS AWS::Lambda::Permission BrainfuckFunctionBrainfuckPermissionPr Resource creation Initiated
od
CREATE_COMPLETE AWS::ApiGateway::Deployment ServerlessRestApiDeployment683b01a6bf -
CREATE_IN_PROGRESS AWS::ApiGateway::Deployment ServerlessRestApiDeployment683b01a6bf Resource creation Initiated
CREATE_IN_PROGRESS AWS::ApiGateway::Stage ServerlessRestApiProdStage -
CREATE_IN_PROGRESS AWS::ApiGateway::Stage ServerlessRestApiProdStage Resource creation Initiated
CREATE_COMPLETE AWS::Lambda::Permission BrainfuckFunctionBrainfuckPermissionPr -
od
CREATE_COMPLETE AWS::ApiGateway::Stage ServerlessRestApiProdStage -
CREATE_COMPLETE AWS::CloudFormation::Stack brainfuck -
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
CloudFormation outputs from deployed stack
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key BrainfuckFunctionIamRole
Description Implicit IAM Role created for Brainfuck World function
Value arn:aws:iam::085583328641:role/brainfuck-BrainfuckFunctionRole-10GJIGA9HAMOU
Key RestApi
Description API Gateway endpoint URL for Prod stage for Brainfuck function
Value https://txs77x92jg.execute-api.us-east-1.amazonaws.com/Prod/brainfuck/
Key BrainfuckFunction
Description Brainfuck World Lambda Function ARN
Value arn:aws:lambda:us-east-1:085583328641:function:brainfuck-BrainfuckFunction-Hh5Y3dLqjbey
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
That means our application is deployed successfully!
Application testing
To test the application, we will use curl:
curl -X POST -H "Content-Type: application/json" -d '{"source":",[.,]","input":"hello"}' https://txs77x92jg.execute-api.us-east-1.amazonaws.com/Prod/brainfuck/ | jq
You should see something like this:
{
"success": "hello"
}
The application will correctly handle errors and return the error message:
curl -X POST -H "Content-Type: application/json" -d '{"source":",[.,"}' https://a6l69d5qx7.execute-api.us-east-1.amazonaws.com/Prod/brainfuck/ | jq
{
"error": "Error parsing source: `Expected end of loop`"
}
curl -X POST -H "Content-Type: application/json" -d '{"INPUT":""}' https://a6l69d5qx7.execute-api.us-east-1.amazonaws.com/Prod/brainfuck/ | jq
{
"error": "failed to parse payload from application/json missing field `source` at line 1 column 12\n"
}
You can also access AWS Cloudwatch to see the generated logs.
START RequestId: b79d600c-3eb1-4623-9072-b19aefecb5b1 Version: $LATEST
END RequestId: b79d600c-3eb1-4623-9072-b19aefecb5b1
REPORT RequestId: b79d600c-3eb1-4623-9072-b19aefecb5b1 Duration: 1.40 ms Billed Duration: 28 ms Memory Size: 128 MB Max Memory Used: 12 MB Init Duration: 26.36 ms
START RequestId: 23023dfd-31dd-4b28-bbe3-ad52ef805d29 Version: $LATEST
END RequestId: 23023dfd-31dd-4b28-bbe3-ad52ef805d29
REPORT RequestId: 23023dfd-31dd-4b28-bbe3-ad52ef805d29 Duration: 1.18 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 12 MB
START RequestId: 42f09c1b-b33c-4b45-8ab3-47489ec4e7ee Version: $LATEST
END RequestId: 42f09c1b-b33c-4b45-8ab3-47489ec4e7ee
REPORT RequestId: 42f09c1b-b33c-4b45-8ab3-47489ec4e7ee Duration: 1.05 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 13 MB
It will not print any logs for us by default because info and debug logs are ignored. However, you can modify the behavior without redeploying the code!
You should follow the steps:
- Open AWS Lambda.
- Open your
Brainfuckfunction. - Go to configuration.
- Open environment variables tab.
- Click edit.
- Enter
RUST_LOGas akeyandDEBUGas avalue. - Save.
Now you should be able to see all the logs generated by our application.
[2022-03-17T11:33:54Z INFO brainfuck_aws] Processing request!
[2022-03-17T11:33:54Z DEBUG brainfuck_aws] Body is valid. Processing request
[2022-03-17T11:33:54Z INFO brainfuck_aws] Interpreter result: Success("hello")
[2022-03-17T11:33:54Z DEBUG hyper::client::pool] reuse idle connection for ("http", 127.0.0.1:9001)
[2022-03-17T11:33:54Z DEBUG hyper::proto::h1::io] flushed 283 bytes
[2022-03-17T11:33:54Z DEBUG hyper::proto::h1::io] parsed 3 headers
[2022-03-17T11:33:54Z DEBUG hyper::proto::h1::conn] incoming body is content-length (16 bytes)
[2022-03-17T11:33:54Z DEBUG hyper::proto::h1::conn] incoming body completed
[2022-03-17T11:33:54Z DEBUG hyper::client::pool] pooling idle connection for ("http", 127.0.0.1:9001)
[2022-03-17T11:33:54Z DEBUG hyper::client::pool] reuse idle connection for ("http", 127.0.0.1:9001)
[2022-03-17T11:33:54Z DEBUG hyper::proto::h1::io] flushed 109 bytes
END RequestId: 549acd63-0bf0-498c-896a-c18e82c9b53e
REPORT RequestId: 549acd63-0bf0-498c-896a-c18e82c9b53e Duration: 1.62 ms Billed Duration: 31 ms Memory Size: 128 MB Max Memory Used: 13 MB Init Duration: 28.76 ms
START RequestId: 2f28bf67-da9a-4a85-bd82-f901370e3fb0 Version: $LATEST
[2022-03-17T11:33:55Z DEBUG hyper::proto::h1::io] parsed 7 headers
[2022-03-17T11:33:55Z DEBUG hyper::proto::h1::conn] incoming body is chunked encoding
Summary
This article continues Brainfuck interpreter series. We implemented CLI and AWS Lambda applications that used the brainfuck_interpreter library from the previous tutorial. It presents to you that Rust is flexible, and the same code can be used anywhere!
You can deploy AWS Lambda application easily with the help of SAM CLI. You can configure the behavior of your application by editing *environment variablesin yourLambda`. Of course, you can deploy your application manually, but be aware that you need to compile it to musl architecture because of the runtime.
You can view the final code in my public repo.
