Skip to content

Commit

Permalink
Merge pull request #5616 from roc-lang/server-example
Browse files Browse the repository at this point in the history
Add basic webserver platform
  • Loading branch information
rtfeldman committed Jun 27, 2023
2 parents e3ab023 + 7df7d51 commit 82a2f3e
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 0 deletions.
2 changes: 2 additions & 0 deletions examples/webserver/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
platform/glue
app
4 changes: 4 additions & 0 deletions examples/webserver/Http.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface Http exposes [request] imports [HttpInternal]

request : Str -> Str
request = \req -> HttpInternal.request req
4 changes: 4 additions & 0 deletions examples/webserver/HttpInternal.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface HttpInternal exposes [request] imports []

request : Str -> Str
request = \req -> req
2 changes: 2 additions & 0 deletions examples/webserver/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
roc glue ../../crates/glue/src/RustGlue.roc platform/glue platform/main.roc
roc build
6 changes: 6 additions & 0 deletions examples/webserver/main.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
app "app"
packages { pf: "platform/main.roc" }
imports []
provides [main] to pf

main = \str -> "hi, \(str)!!"
38 changes: 38 additions & 0 deletions examples/webserver/platform/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ⚠️ READ THIS BEFORE MODIFYING THIS FILE! ⚠️
#
# This file is a fixture template. If the file you're looking at is
# in the fixture-templates/ directory, then you're all set - go ahead
# and modify it, and it will modify all the fixture tests.
#
# If this file is in the fixtures/ directory, on the other hand, then
# it is gitignored and will be overwritten the next time tests run.
# So you probably don't want to modify it by hand! Instead, modify the
# file with the same name in the fixture-templates/ directory.

[package]
name = "host"
version = "0.0.1"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
edition = "2018"
links = "app"

[lib]
name = "host"
path = "src/lib.rs"
crate-type = ["staticlib", "rlib"]

[[bin]]
name = "host"
path = "src/main.rs"

[dependencies]
roc_std = { path = "glue/roc_std", features = ["std"] }
roc_app = { path = "glue/roc_app" }
libc = "0.2"
hyper = { version = "0.14", features = ["http1", "http2", "client", "server", "runtime", "backports", "deprecated"] }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
futures = "0.3"
bytes = "1.0"

[workspace]
9 changes: 9 additions & 0 deletions examples/webserver/platform/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
fn main() {
#[cfg(not(windows))]
println!("cargo:rustc-link-lib=dylib=app");

#[cfg(windows)]
println!("cargo:rustc-link-lib=dylib=libapp");

println!("cargo:rustc-link-search=.");
}
14 changes: 14 additions & 0 deletions examples/webserver/platform/host.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// ⚠️ READ THIS BEFORE MODIFYING THIS FILE! ⚠️
//
// This file is a fixture template. If the file you're looking at is
// in the fixture-templates/ directory, then you're all set - go ahead
// and modify it, and it will modify all the fixture tests.
//
// If this file is in the fixtures/ directory, on the other hand, then
// it is gitignored and will be overwritten the next time tests run.
// So you probably don't want to modify it by hand! Instead, modify the
// file with the same name in the fixture-templates/ directory.

extern int rust_main();

int main() { return rust_main(); }
9 changes: 9 additions & 0 deletions examples/webserver/platform/main.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
platform "webserver-platform"
requires {} { main : _ }
exposes []
packages {}
imports []
provides [mainForHost]

mainForHost : Str -> Str
mainForHost = \str -> main str
210 changes: 210 additions & 0 deletions examples/webserver/platform/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
use futures::{Future, FutureExt};
use hyper::{Body, Request, Response, Server, StatusCode};
use roc_app;
use roc_std::RocStr;
use std::cell::RefCell;
use std::convert::Infallible;
use std::net::SocketAddr;
use std::os::raw::{c_int, c_long, c_void};
use std::panic::AssertUnwindSafe;
use tokio::task::spawn_blocking;
use libc::{sigaction, siginfo_t, sigemptyset, SIGBUS, SIGFPE, SIGILL, SIGSEGV, sighandler_t, sigset_t, SA_SIGINFO, SIG_DFL};

const DEFAULT_PORT: u16 = 8000;

// If we have a roc_panic or a segfault, these will be used to record where to jump back to
// (a point at which we can return a different response).
thread_local! {
// 64 is the biggest jmp_buf in setjmp.h
static SETJMP_ENV: RefCell<[c_long; 64]> = RefCell::new([0 as c_long; 64]);
static ROC_CRASH_MSG: RefCell<RocStr> = RefCell::new(RocStr::empty());
static SIGNAL_CAUGHT: RefCell<c_int> = RefCell::new(0);
}

extern "C" {
#[link_name = "setjmp"]
pub fn setjmp(env: *mut c_void) -> c_int;

#[link_name = "longjmp"]
pub fn longjmp(env: *mut c_void, val: c_int);
}

unsafe extern "C" fn signal_handler(sig: c_int, _: *mut siginfo_t, _: *mut libc::c_void) {
SIGNAL_CAUGHT.with(|val| {
*val.borrow_mut() = sig;
});

SETJMP_ENV.with(|env| {
longjmp(env.borrow_mut().as_mut_ptr().cast(), 1);
});
}

fn setup_signal(sig: c_int) {
let sa = libc::sigaction {
sa_sigaction: signal_handler as sighandler_t,
sa_mask: sigset_t::default(),
sa_flags: SA_SIGINFO,
};

let mut old_sa = libc::sigaction {
sa_sigaction: SIG_DFL,
sa_mask: sigset_t::default(),
sa_flags: 0,
};

unsafe {
sigemptyset(&mut old_sa.sa_mask as *mut sigset_t);
sigaction(sig, &sa, &mut old_sa);
}
}

fn call_roc(req_bytes: &[u8]) -> Response<Body> {
let mut setjmp_result = 0;

SETJMP_ENV.with(|env| {
setjmp_result = unsafe { setjmp(env.borrow_mut().as_mut_ptr().cast()) };
});

if setjmp_result == 0 {
setup_signal(SIGSEGV);
setup_signal(SIGILL);
setup_signal(SIGFPE);
setup_signal(SIGBUS);

let req_str: &str = std::str::from_utf8(req_bytes).unwrap(); // TODO don't unwrap
let resp: String = roc_app::mainForHost(req_str.into()).as_str().into();

Response::builder()
.status(StatusCode::OK) // TODO get status code from Roc too
.body(Body::from(resp))
.unwrap() // TODO don't unwrap() here
} else {
let mut crash_msg: String = String::new();
let mut sig: c_int = 0;

SIGNAL_CAUGHT.with(|val| {
sig = *val.borrow();
});

if sig == 0 {
ROC_CRASH_MSG.with(|env| {
crash_msg = env.borrow().as_str().into();
});
} else {
crash_msg = "Roc crashed with signal {sig}".into(); // TODO print the name of the signal
}

Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(crash_msg))
.unwrap() // TODO don't unwrap() here
}
}

async fn handle_req(req: Request<Body>) -> Response<Body> {
match hyper::body::to_bytes(req.into_body()).await {
Ok(req_body) => {
spawn_blocking(move || call_roc(&req_body))
.then(|resp| async {
resp.unwrap() // TODO don't unwrap here
})
.await
}
Err(_) => todo!(), // TODO
}
}

/// Translate Rust panics in the given Future into 500 errors
async fn handle_panics(
fut: impl Future<Output = Response<Body>>,
) -> Result<Response<Body>, Infallible> {
match AssertUnwindSafe(fut).catch_unwind().await {
Ok(response) => Ok(response),
Err(_panic) => {
let error = Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body("Panic detected!".into())
.unwrap(); // TODO don't unwrap here

Ok(error)
}
}
}

const LOCALHOST: [u8; 4] = [127, 0, 0, 1];

async fn run_server(port: u16) -> i32 {
let addr = SocketAddr::from((LOCALHOST, port));
let server = Server::bind(&addr).serve(hyper::service::make_service_fn(|_conn| async {
Ok::<_, Infallible>(hyper::service::service_fn(|req| handle_panics(handle_req(req))))
}));

println!("Listening on <http://localhost:{port}>");

match server.await {
Ok(_) => 0,
Err(err) => {
eprintln!("Error initializing Rust `hyper` server: {}", err); // TODO improve this

1
}
}
}

#[no_mangle]
pub extern "C" fn rust_main() -> i32 {
match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime.block_on(async { run_server(DEFAULT_PORT).await }),
Err(err) => {
eprintln!("Error initializing tokio multithreaded runtime: {}", err); // TODO improve this

1
}
}
}

// Externs required by roc_std and by the Roc app

#[no_mangle]
pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
return libc::malloc(size);
}

#[no_mangle]
pub unsafe extern "C" fn roc_realloc(
c_ptr: *mut c_void,
new_size: usize,
_old_size: usize,
_alignment: u32,
) -> *mut c_void {
return libc::realloc(c_ptr, new_size);
}

#[no_mangle]
pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
return libc::free(c_ptr);
}

#[no_mangle]
pub unsafe extern "C" fn roc_panic(msg: RocStr) {
// Set the last caught signal to 0, so we don't mistake this for a signal.
SIGNAL_CAUGHT.with(|val| {
*val.borrow_mut() = 0;
});

ROC_CRASH_MSG.with(|val| {
*val.borrow_mut() = msg;
});

SETJMP_ENV.with(|env| {
longjmp(env.borrow_mut().as_mut_ptr().cast(), 1);
});
}

#[no_mangle]
pub unsafe extern "C" fn roc_memset(dst: *mut c_void, c: i32, n: usize) -> *mut c_void {
libc::memset(dst, c, n)
}
3 changes: 3 additions & 0 deletions examples/webserver/platform/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
std::process::exit(host::rust_main() as _);
}

0 comments on commit 82a2f3e

Please sign in to comment.