Hacker News new | past | comments | ask | show | jobs | submit login
Pencil: A Microframework Inspired by Flask for Rust (fengsp.github.io)
231 points by fsp on March 8, 2016 | hide | past | favorite | 57 comments



Copying my comment on this from Reddit:

Great, I'll definitely have to try this! I've used several Rust web frameworks, including Iron[1], Nickel[2], and Rustful[3]. Each of them has their own strengths and weaknesses, so I've been launching new web sites using all of them to see what fits me best. (Until now, Rustful felt the most comfortable. But I like the others too!) Now I have one more thing to evaluate!

Before diving into this, one thing that caught me at a glance is the order of the arguments in the routing rules. It is using `/user/<int:user_id>`, but I think `/user/<user_id:int>` is more natural. A small difference, but I believe this is one thing that Bottle[4] did right. When types come later, it is easier to omit them if there's a default (e.g. `<username>` has the same meaning as `<username:str>`) and it is also consistent with the existing type annotation syntax of Rust. I suspect Flask uses the current order only because of the backward compatibility concerns. But only mitsuhiko can tell for sure.

Also, it would be great if `ViewFunc` is extended to accept the functions other than the ones returning `Result<Response, _>`, e.g. `Result<String, _>` and `Result<Vec<u8>, _>`. It is true that there exist the `From` implementations in `Response`, but it is generally more convenient to return `String` directly from the handlers rather than manually forming `Response::from("...")`. I think I can prepare a PR if you like!

Thanks again for another attempt to use Rust as a web language!

[1] http://ironframework.io/

[2] http://nickel.rs/

[3] https://github.com/Ogeon/rustful

[4] http://bottlepy.org/


For those reading, note that mitsuhiko did end up responding to this on Reddit: https://www.reddit.com/r/rust/comments/49jv81/introducing_pe...


I've found that whenever I start a small web project in a new language, I always look for the "Flask" of that language. I love using a framework that gives the bare essentials and nothing else.

I don't use Rust, but your code examples look great! I understand that Rust has less opportunity for "magic" to clean things up (for example, in Python you can use a decorator to specify a route for a function), but otherwise it looks very clear. Awesome work!


There's probably some opportunity to use custom annotations in Rust to achieve the same thing.

    #[route("/foobar")]
    fn user(_: &mut Request) -> PencilResult {
        // ...
    }
However, procedural macros are unstable, but Rust is able to do amazing things with macros.


I know very little Rust, but I was actually looking at how this could be done in Rust some time ago, but I didn't get anything working. All the routes (e.g. /foobar and accompanying function) needs to be collected by the "router" somehow which begs for a global route-container that the route macro (like Flasks route decorator) would need to add routes to, similar to how it is done is Flask, and, well, yeah, I couldn't find a rusty way to do it.

Bearing in mind that macros er unstable, can this be done in Rust, and if so, what would the process be?


There are a few different ways one could achieve this. The first options would be to generate a struct:

    struct RouteUser {
        route: &'static str,
        f: fn(_: &mut Request) -> PencilResult
    }
Then given

    #[route("/user")]
    fn user(_: &mut Request) -> PencilResult {}
You would hijack the `user` function to return an instance of the struct.

    fn user() -> RouteUser {
        // The actual function the user wrote:
        fn user(_: &mut Request) -> PencilResult {
            // ...
        }

        RouteUser {
            // Generated from the annotation argument
            route: "/user",
            f: user
        }
    }
There is a difference between item decorators (`#[foobar]`) and regular procedural macros and I'm not completely sure if you could in-fact significantly change the given function. I haven't touched procedural macros in a while.

To use the above route, you would simply have a `Route` trait perhaps.

    trait Route {}
And implement it for each generated struct:

    impl Route for RouteUser {}
Then you could use the route as

    app.route(user());
Which could be defined as

    fn route<R>(r: R) where R: Route {
        // ...
    }


That seems like a good approach, although one thing is missing: when you use the route-decorator in Flask, the route is registered to the app object "via magic", whereas in your example, you still have to register it manually (`app.route(user())`). Now, I am partial to liking registering manually, like in your example and in Pencil itself, since that's clearer than having a lot of routes spread around in a whole lot of files that are magically registered, but just for the kicks, would it be possible to not having to manually register the route to the app object?


I'm a huge fan of flask but I actually prefer the approach of not using decorators. Without decorators its easy to see all the paths in one area and they get type-checked in rust.


There is a language plugin for pythonic decorators in Rust:

https://github.com/Manishearth/rust-adorn


You actually can use pythonish decorators in Rust, I wrote a proof of concept library for that: https://github.com/manishearth/rust-adorn (it's not really intended to be used, but if folks want to use it I'm willing to clean it up)


>I always look for the "Flask" of that language.

I always look for the "Sinatra" of that language.


> "gives the bare essentials and nothing else"

Rust is that thing exactly.


Probably a good idea to add "web development framework" somewhere on the page. It'll not only improve your Search engine optimization. But it also will be more explicit for the visitors.


Anyone know of any benchmarks? This seems to be built on top of hyper. I remember checking out hyper a couple months ago and being disappointed with its performance. Last I checked it was using synchronous IO, and was performing about an order of magnitude worse than equivalent Go. That could certainly change, but I'm hesitant to use Rust for an HTTP server like I would with Go until I see better performance.


Switching to asynchronous I/O isn't going to magically result in better performance on HTTP workloads. I don't think most of what any performance difference you're seeing is due to that: I suspect instead that it's relatively "boring" optimization work that has yet to be done in Hyper.

The primary difference between async and synchronous I/O is (a) better memory usage due to not having a stack per connection; (b) you can avoid the overhead of context switches to wake up an I/O thread; (c) thread spawning performance is faster due to the kernel not having to be involved. Golang's advantages in (a) and (b) are much less than what is typically thought of as "async I/O", because it still semantically uses a thread-per-connection and so is performing the same operations that a synchronous I/O implementation performs, just with a different implementation strategy.


Go's networking library actually does call epoll/kqueue/etc on the backend, so it is using nonblocking io. Using nonblocking IO will provide better performance because it will increase the number of concurrent requests it can serve. It's not a silver bullet, and can be very difficult to implement at the application layer to avoid blocking, but it will give markedly better performance.


But one of the reasons you're able to process more with async I/O is bypassing the costs of the thread-per-connection model. So you're not forced to store all the thread's stacks and you don't have expensive context switches.

As the parent illustrated, even with Go using nonblocking I/O, it's perceived benefits in that area isn't that great because Go still semantically has a thread-per-connection. So the performance characteristics of Go isn't simply async vs sync I/O.


>But one of the reasons you're able to process more with async I/O is bypassing the costs of the thread-per-connection model.

One reason, not the only, or the most important. The primary advantage of non blocking IO is that the CPU isn't sitting idle while waiting for an IO operation to complete. We aren't wasting an entire core to write the response.


That's not how blocking I/O works! Sitting in a busy loop on the CPU burning power polling the I/O device was, like, how the Apple II may have worked, but it hasn't worked like that on any major OS since at least 1990. Operating systems perform context switches on I/O.


> Operating systems perform context switches on I/O.

which are very cheap :)


And if you accept that (which I'm not sure I do, but anyhow) then you accept that there isn't much difference between userspace M:N and blocking 1:1 threading.


> Go's networking library actually does call epoll/kqueue/etc on the backend, so it is using nonblocking io.

I know, of course. If you reread my comment, you'll notice that it wasn't focusing on the particular syscall that the system uses.

> Using nonblocking IO will provide better performance because it will increase the number of concurrent requests it can serve.

Because of (a), (b), and/or (c), which I addressed in my comment above.


Right, so Hyper would be faster if it used true asynchronous I/O, right? That was my whole point. I just brought up Go so I had something to compare it to.


Hyper may very well be much faster, but it isn't. Which is the whole issue with the Rust vs Go debates: one is possibly much better, but the other one gets all the code written for it in 1/6th of the time (Rust came out the same time Go did, to great fanfare).

Are we willing to wait 6x longer for rust to catch up? Some people are, most people aren't.


I don't know whether you're talking about a 6x development speed differential or a 6x performance difference between Rust and Go HTTP stacks or what, but I am pretty sure that by making up numbers like that you're language warring for no reason.


yes, in this case i am language warring for no reason. i'm an unapologetic go devotee but i like rust just the same. i wish people would just get on with things and write software, rather than comment on minutiae on message boards.


That's nice and all, but unfortunately the real world tests I've seen (and done) show something like Netty completely trumping anything available for Rust.


1. Are you testing against mio? If so, have you filed bugs against it? At a high level, there is no fundamental design difference between Netty and mio.

2. Netty is worlds away from Golang, precisely for the reasons stated above. Go is a userspace, M:N implementation of per-thread, blocking I/O, while Netty is a truly asynchronous I/O framework. Of course truly async I/O can beat synchronous I/O, but the entire point is that Golang is not strictly async I/O.



Hyper's async IO is being actively worked on or done IIRC.


Great start. Instead of numeric codes, perhaps use an enum for all the acceptable status codes?

As Jane Street Says, make illegal state unrepresentable.


This is a good slogan, but there's some subtleties when it comes to HTTP status codes. That is, there are the defined ones, but any number is legit, so you end up with an enum with a member that's basically "anything we don't know about", so it's not as clear-cut as it is in other situations.


Good point, this is where Rust's flexible enums are an advantage relative to C-style. If you'll forgive the Haskell syntax, you can have something like:

    data StatusCoode
      = Ok200
      | Missing404
      | Error500
      | OtherStatusCode Int
To rephrase the slogan a bit - make an illegal state a bit more difficult to represent or at least the risk of an illegal state clear to the programmer.



My bad.. I was just basing my comment off the readme. In general, I love what you guys are doing and my team is looking closely at Rust and Leaf.


No worries! I just meant to confirm your suspicions :)


And of course a problem with this is any status code now has two representations: the canonical enum variant, and the catch-all variant with the same numeric value.


Looks good! I don't know how I feel about using OK for anything but 200 responses though.


Well, 100- and 300- class respones aren't errors, but yeah, 400- and 500- might make more sense as Err.

You could also look at it from another point-of-view. Ok() means that I am able to fully handle this request, even if it's an HTTP error code, and Err being "nope, don't know. Sorry bud".


`Ok` is a member of the Rust `Result` enum: https://doc.rust-lang.org/std/result/enum.Result.html (and `PencilResult` as well)

Since it's integral to error handling in Rust you probably can't get away from it.


This requires less boilerplate than Iron (which is built upon Hyper). I'll definitely be checking this out.

That said, I've been very happy with Iron thus far.


Although Iron is powerful, I find there is a lot of complexity around it. The middleware concept mostly contributes to this in addition to the abstractions over Hyper constructs.


That looks so simple. Even simple enough for embedded systems. I like it! Maybe I need to look at Flask, too.


Flask is my favorite tool for writing web apps in python. Django is nice, but I like the idea of building a framework up around the application, instead of shoehorning an application into a framework, and Flask lets you only bring in the parts you need.


That's exactly my thought. Also helps with portability and long-term maintenance.


https://github.com/fengsp/pencil/blob/master/examples/hello/...

    app.get("/", "hello", hello);
    app.get("/user/<int:user_id>", "user", user);
    ....
This reminds me of Pyramid more than Flask though, just my personal observation. The code looks pretty readable even from a non-Rust programmer.


This is great! It looks like I'll begin my Rust journey earlier than expected.


This look like some amazing work.

I would love to see how Rust will influence the web development sphere and I'd like to see if it would cut down on some common bugs that are made in production web development.


The magic of Flask is that that thing is full of state and globals you just import and they work, like request, app, session.


That is also, I think, one of flask's biggest weaknesses. It does make everything simpler though. I'm just not very fond of magic globals. It also doesn't fit very well into REST and HTTP. On the other hand, I love flask for just about everything else.


Very nice and readable. What are the plans for this? Anyone share to comment if there is a development roadmap or if its just an exercise in programming.


https://github.com/fengsp/pencil/graphs/contributors

It looks like it's a bit of a pet project at the moment, but these sorts of things have a habit of taking off in a big way sometimes. Probably a big "it depends".


It does look simpler than using Go for a similar type of project, IMO.


Missed opportunity, should've called it:

"The Blueprint²: The Gift & the Curse"


isn't flask inspired by a WSGI framework in ruby originally? Like Sinatra?

Hum, yes, that is what the author says :)


That's a strange license to use


CC is fine for docs.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: