iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
💍

Onyx Introduction 3: Writing a Web Server and Deploying to Wasmer Edge

に公開

This is a continuation of https://zenn.dev/hatappo/articles/09ff4f8546f805, but I've designed it so that it can be read and tried as a standalone article as much as possible.


I will proceed by referring to the official guide here:

https://onyxlang.io/docs/guides/http-server

I will use WCGI on Wasmer Edge. Wasmer Edge is an edge computing platform provided by Wasmer. WCGI is a mechanism that allows Wasm to be used like CGI.

https://wasmer.io/posts/announcing-wcgi

Preparation

Please install the Onyx CLI:

Also, install the Wasmer CLI:

  • brew install wasmer (Example installation for Mac)

Project

Create a working directory and set it up as an Onyx project.

$ mkdir -p my-http-server && cd $_

$ onyx package init
Creating new project manifest in ./onyx-pkg.kdl.

Package name:
Package description:
Package url:
Package author:
Package version (0.0.1):

The init command creates a configuration file named onyx-pkg.kdl. It's similar to package.json in npm, where dependency libraries and other information are defined.

Add the dependency for the http-server package and download it. onyx package sync is equivalent to npm install in npm.

$ onyx package add http-server
       Added  'http-server' version 0.2.24

$ onyx package sync
       Fetch  http://github.com/onyx-lang/pkg-http-server  0.2.24

$ onyx package show
Package name        :
Package description :
Package url         :
Package author      :
Package version     : 0.0.1

Dependencies:
    http-server | Dependency { name = "http-server", version = 0.2.24, source = Git("http://github.com/onyx-lang/pkg-http-server") }

Minimal Code

#load "./lib/packages"

use http
use http.server {
    Req :: Request,
    Res :: Response,
}

main :: () {
    router := http.server.router();

    // Register a handler for 'GET /'
    router->get("/", (req: &Req, res: &Res) {
        res->html("HTTP Server in Onyx!");
        res->status(200);
        res->end();
    });

    app := http.server.tcp(&router);
    app->serve(8000);
}
  • #load "./lib/packages" allows you to globally load libraries brought in by onyx package sync. The path ./lib/packages is resolved to point to ./lib/packages.onyx. The ./lib/packages.onyx file should contain a list of #load statements for each package brought in by onyx package sync, generated by the onyx package command.
    • Since only packages.onyx exists directly under .lib, using #load_all "./lib" is also OK[1].
  • In the use statements, Req :: Request is simply aliasing the function while importing it. You can use it without an alias as long as the symbols used in the subsequent code match.

The code that follows should be relatively easy to understand, as it follows a typical pattern for web applications. The flow is: create a router, add a routing rule, create an HTTP server using that as input, and listen on port 8000.

Local Verification Part 1

Just run onyx run.

$ onyx run main.onyx
[Info][HTTP-Server] Serving on port 8000

Furthermore, check the HTTP response by opening http://localhost:8000/ in a browser or similar.

$ curl http://localhost:8000
HTTP Server in Onyx!%

Local Verification Part 2

Next, we will run it with Wasmer and WASIX.

$ onyx build main.onyx -r wasi -DWASIX -o main.wasm

$ wasmer run --net main.wasm
[Info][HTTP-Server] Serving on port 8000

You can also check the response at localhost:8000.

$ curl http://localhost:8000
HTTP Server in Onyx!%
  • onyx build
    • The -r option specifies the runtime. The default is onyx, and you can also specify wasi, js, or custom.
    • Although the -D option is not mentioned in the documentation, in this case, it seems to specify the inclusion of WASIX extensions[2].
    • The -o option specifies the name of the generated Wasm binary. It defaults to out.wasm if not specified.
  • The --net option for wasmer run is an instruction to enable the network features of the host computer. The fact that network features cannot be used unless explicitly specified might be a reflection of the Wasm specification.

Refactoring

We will move the routing that was written inline in the main function out into its own function. By using the #tag directive, the routing function can be discovered.

main2.onyx

#load "./lib/packages"

use http
use http.server {
    Req :: Request,
    Res :: Response,
    route // (1)
}

#tag route.{ .GET, "/" } // (2)
index :: (req: &Req, res: &Res) {
    res->html("HTTP Server in Onyx!");
    res->status(200);
    res->end();
}

main :: () {
    router := http.server.router();
    router->collect_routes(); // (3)
    app := http.server.tcp(&router);
    app->serve(8000);
}
  • (1): Added a new statement to load the route function.
  • (2): Passing the route function to the #tag directive. It feels like decorators in JS or Python, or annotations in Java.
  • (3): The collect_routes function collects the routes defined with the #tag directive and sets them in the router. This makes the routing configuration pluggable[3].

Check the behavior as before.

$ onyx run main2.onyx
[Info][Http-Server] Serving on port 8000

$ curl http://localhost:8000
HTTP Server in Onyx!

Deploying to Wasmer Edge

main3.onyx

#load "./lib/packages"

use http
use http.server {
    Req :: Request,
    Res :: Response,
    route
}

#tag route.{ .GET, "/" }
index :: (req: &Req, res: &Res) {
    res->html("HTTP Server in Onyx!");
    res->status(200);
    res->end();
}

main :: () {
    router := http.server.router();
    router->collect_routes();
    http.server.cgi(&router); // (1) Switching from http.server.tcp to http.server.cgi
}
  • (1): http.server.tcp(&router) was changed to http.server.cgi(&router), and the code for starting the server with app->serve(8000) that followed it has been removed. Note also that since the return value of http.server.tcp is no longer used, receiving it in a variable has been removed as well. Everything else is identical to main2.onyx.

Build the project.

$ onyx build main3.onyx -r wasi -o my-http-server.wasm 

Add a configuration file named wasmer.toml [4]. This is a manifest file for publishing to the Wasmer Registry, containing dependency packages, metadata, and commands to be executed. If writing from scratch, you can use the wasmer init command.

[package]
name = "hatappo/my-http-server"
version = "0.1.0"
description = "My first HTTP server"
license = "MIT"

[[module]]
name = "server"
source = "my-http-server.wasm"
abi = "wasi"

[[command]]
name = "server"
module = "server"
runner = "https://webc.org/runner/wcgi"

[command.annotations.wasi]
env = ["SCRIPT_NAME=rust_wcgi"]

[command.annotations.wcgi]
dialect = "rfc-3875"
  • Change package.name to suit your environment or preference.
$ wasmer login
Opening auth link in your default browser: https://wasmer.io/auth/cli?nonce_id=xxxxx&secret=xxxxx
Waiting for session... Done!
 Login for Wasmer user "xxxxx" saved

If you don't have an account, you can create one in the browser.

Also, payment settings are required, so go to https://wasmer.io/payment/setup beforehand to register your card information. Please note the warnings that it is free during the Beta phase [5] and that registering payment information won't lead to unexpected usage charges without your permission.

Create a deployment configuration file.

$ wasmer app create
App type: HTTP server
Who should own this package?: hatappo
Found local package: 'hatappo/my-http-server@0.1.1'
Use package 'hatappo/my-http-server' yes
What should be the name of the app? <NAME>.wasmer.app: xxxxx-my-http-server
Would you like to publish the app now? no
Writing app config to '/xxxxx/lang-onyx/my-http-server/app.yaml'
To (re)deploy your app, run 'wasmer deploy'
  • Select HTTP server for App type. The rest can mostly be left as defaults.

Deploy the application.

$ wasmer deploy                                       
Loaded app from: /xxxxx/my-http-server/app.yaml

Publish new version of package 'hatappo/my-http-server'? yes
Publishing package...

[1/2] ⬆️   Uploading...
[2/2] 📦  Publishing...
Successfully published package `hatappo/my-http-server@0.1.7`
Waiting for package to become available.......
Package 'hatappo/my-http-server@0.1.7' published successfully!

Deploying app xxxxx-my-http-server...

 App hatappo/xxxxx-my-http-server was successfully deployed!

> App URL: https://xxxxx-my-http-server.wasmer.app
> Versioned URL: https://xxxxx.id.wasmer.app
> Admin dashboard: https://wasmer.io/apps/xxxxx-my-http-server

Waiting for new deployment to become available...
(You can safely stop waiting now with CTRL-C)
..
New version is now reachable at https://xxxxx-my-http-server.wasmer.app
Deployment complete
  • App URL: https://*.wasmer.app → A URL specific to the deployed application.
  • Versioned URL: https://*.id.wasmer.app → A unique URL for each deployment version.
  • Admin dashboard: https://wasmer.io/apps/* → The URL for the application's management dashboard.

Success if "HTTP Server in Onyx!" is displayed when you access the Versioned URL or App URL! 🎉

Supplement - app.yaml

A deployment configuration file named app.yaml should have been generated by wasmer app create [6].

---
kind: wasmer.io/App.v0
name: xxxxx-my-http-server
package: hatappo/my-http-server
debug: false
  • kind specifies the type and version of this configuration file, but currently, it is fixed to wasmer.io/App.v0, and name is an arbitrary name, so essentially it's not setting much of anything here.

Supplement - File structure at the time of deployment completion

The file structure at this point should look like the following. The files/directories marked with are required for deployment to Wasmer Edge.

$ tree -L 1
.
├── app.yaml             # ★
├── lib
├── main.onyx
├── main2.onyx
├── main3.onyx
├── my-http-server.wasm  # ★
├── onyx-pkg.kdl         # ★
└── wasmer.toml          # ★

Supplement - Debugging the deployment

If the deployment doesn't go well, please check your code and try wasmer deploy again.
In my case, I had written:

http.server.cgi(&router);

as

app = http.server.cgi(&router);

and it initially failed with a 400 error, likely due to a compilation error on the Wasmer side.

Supplement - Updating various files by wasmer deploy

  • The wasmer deploy command may automatically rewrite package version numbers or app_id in the local app.yaml and wasmer.toml. It was a bit hard to imagine how to handle this in a deployment pipeline. It might be okay to ignore it, though.

Summary

I created a simple web application using the pkg-http-server library and deployed it to Wasmer Edge. pkg-http-server feels similar to Express in Node.js, and routing seems straightforward to write.

Deploying to Wasmer Edge is quite fast, taking only a few seconds. The command-line tools are well-developed and easy to use. However, investigating errors when they occur might be a bit difficult. Since it's still in Beta, we can definitely look forward to future improvements.

I'm also happy that local execution, including the Onyx CLI, is easy. However, I felt it was very "serverless-development-like" in that I wasn't sure how to start it locally in CGI mode (I haven't looked into it properly yet).

脚注
  1. The #load_all directive loads all *.onyx files in the specified directory. It does not recursively read subdirectories. ↩︎

  2. Implementation code is around here -> https://github.com/onyx-lang/onyx/blob/31d7afa31165c104224949ed44949a43796f8021/compiler/src/onyx.c#L280-L291 ↩︎

  3. The implementation code for how collect_routes gathers routing can be found here. It seems to scan globally for tags where the route function is used, even across different files, but both route and collect_routes have an optional group argument that allows you to narrow the scope. ↩︎

  4. Documentation -> https://docs.wasmer.io/registry/manifest ↩︎

  5. https://wasmer.io/posts/wasmer-edge-beta-is-ga ↩︎

  6. Documentation -> https://docs.wasmer.io/edge/configuration/app-configuration ↩︎

Discussion