Hacker News new | past | comments | ask | show | jobs | submit login
Rust for the Web (thefullsnack.com)
244 points by huydotnet on Aug 15, 2017 | hide | past | web | favorite | 95 comments



Web developers may be also interested in https://gotham.rs/, which was released very recently and looks to be a promising competitor to Rocket.


And for those who care, unlike Rocket it runs on stable rust. IMO that is useful as I personally prefer running on stable than nightly.


Yeah, Gotham looks promising, and it has even better document than Rocket. It's always good to see more competitors coming around.


I'm one of the developers of Gotham. Don't hesitate to reach out if you have any questions about it.


Great to see a few new web frameworks popping up.

It seems that the older frameworks like Iron and Nickel.rs are not (yet) catching the async train.


Man Rust is super hard already! I mean people told me it would be harder to think functionally but after clojure, i think lisps are super easy. But i feel Rust is way harder than anything. Infact i feel Haskell is comparatively easier than Rust. So i am not sure why one wants to use it for web. I think Rust has a place and that is to replace C++ for system software, possibly even C for writing Kernels because why not? But definitely not for web. Go is probably the right choice for that sort of performance scenario in the backend. But for frontend neither Go or Rust are good simply because of the huge size of the runtimes. Clojure-script comparatively has a smaller runtime than either Go or Rust runtimes compiled to JS.


> But for frontend neither Go or Rust are good simply because of the huge size of the runtimes.

Rust has a comparable amount of runtime as C, here it is: https://github.com/rust-lang/rust/blob/master/src/libstd/rt....

wasm/asmjs does bring in some stuff, because it's mostly in a "it works" state than a "this is as good as it gets" state; our initial implementations didn't even do much optimization at all. Last I checked (last November), an example TodoMVC was to ~650K total, and given that the smallest known Rust binary is 151 bytes, I'd imagine that only gets smaller into the future.

https://cryze.github.io/advent-of-code-2016/ has a bunch of Advent of Code programs in Rust, compiled this way; Day 1 is weighing in at 300kb, it looks like.

asmjs is also much larger than wasm, day 3 is showing 1.4 MB for asm, but 69kb for wasm.


I disagree. I think Rust is pretty easy once you've spent a bit of time learning about lifetimes.

It should be substantially less surprising when it's got non-lexical lifetimes, though.

Rust seems to me eminently suited to write high-performance web applications.


Let's be serious, Rust will never be as easy to use as a language with GC. It will perform better on many metrics, but you have to pay that cost.


Unless you care about resources other than memory. GC doesn't help to make sure mutexes don't deadlock and files are closed. As far as I know there is no general purpose algorithm for those.


Even still, you can opt-in to those features in a GC language when you need it, but the contrary is not the case in Rust at the moment.


But you can't opt out of the problems with garbage collectors without dropping the language.


Most common GC "problems" have fairly simple solutions. If we're talking about 50GB+ heaps, then we're already talking about a problem that's getting significant engineering investment, so custom solutions become warranted, ie. a special purpose library that manages its own resources that plugs into the safe runtime, similar to unsafe Rust, just less integrated.


Rust will never be as convenient as GC languages for web, GUI and distributed programming.

Also just because they have a GC doesn't mean all allocations have to go through the heap.

Rust killer area is the low level C stuff, resource constrained embedded devices, device drivers, codecs, ....


Affine type system + the borrowck (each value has a single owner & you can't mutate it from more than one place at a time) which are the mechanisms that Rust uses to avoid GC are also a huge help when structuring a program whit predictible behavior : it's the perfect middle-line between imutable data structures (which are slow due to copying and have anoying limitations in many cases) and open-bar mutate-everything models. It's a really powerful feature which helps a lot to have a code easy to understand and to maintain. This is a massive productivity bonus IMHO and no GC will ever give you that.

Lifetime annotations (which are the other feature needed by Rust's memory management) are sometime annoying though.


A GC is way more productive than the metal effort and programming skills required to master an affine type system, just to put a couple of buttons on the screen talking to a database backend.

I had lots of fun going through the Idris and Rust books, but I don't see the average enterprise programmer wanting to deal with it.

Surely not the ones that I know, that don't have any idea what Idris, Rust or even HN are all about.

Which is why D, Swift, C++, Pony designers are looking into Rust, taking some of its ideas, and blending them with GC + dataflow analysis.


> A GC is way more productive than the metal effort and programming skills required to master an affine type system

Ease of learning and productivity are two different things. Rust is indeed harder to learn than JavaScript, but IMHO it's more productive in a day-to-day use thanks to its powerful type system (caveat, the ecosystem is still pretty young).

Swift is harder to use than Rust in my opinion since you have to deal with cyclic references to avoid memory leak with ARC.


Haskell, OCaml, Scala and C++ have a more powerful type system, while being more productive than Rust on its current state.

I share the opinion of the current thread going on Reddit and Rust forum, that the use cases of C and C++ should be the ones being targeted by Rust.

Everyone else is quite happy using GC based languages, specially on web development.


Haskell and OCaml look alien and are beyond Rust in term of learning curve, and I speak as someone who historically learned OCaml as his first language (just French things). I can't say anything about Scala. I don't know what C++ is doing in this list since there's no GC and its type system doesn't really compare to the other languages discussed here (including Rust).

> Everyone else is quite happy using GC based languages

The huge amount of rustaceans coming from language with GC (including myself) isn't really in favor of your argument.

As a web dev currently using rust for cli tools, I'd be really happy to do my web stuff in Rust also once the web story for rust has matured. And seeing what happened since last year in this field, I'm really optimistic for the future.

I understand that not everybody thinks the way I do, but we exist though ;).


I learned Caml Light a few years before Objective Caml was designed.

C++ has a GC API since C++11 and there are extensions like C++/CLI or Unreal C++ that make use of a GC.

The amount of people doing production web development in Rust won't be more than a single digit, when one takes the whole web community into account.

I surely don't want to think about affine types when doing UI design in Storyboards, Android designer, WPF/UWP Blend designer.


> The amount of people doing production web development in Rust won't be more than a single digit

Well, I'm pretty sure a few people at INRIA would be really happy if OCaml was used by a single digit percentage of the whole web community. If Rust reached this stage I think we could talk about its success in this field. Go is considered a pretty sucessful language for web back-end stuf and I'm pretty sure it's not used by more than a single digit percentage of web devs.

> I surely don't want to think about affine types when doing UI design in Storyboards, Android designer, WPF/UWP Blend designer.

I don't understand your point, affine type is something that helps you write more maintainable code : you can't have two different sections of your code mutating the same piece of data without you knowing it. It makes me think less actually. And if you need to have two owners for a given piece of data, you can opt-out the ownership system with reference counting and interior mutability. Those rules and mechanisms are difficult to learn and internalize, but once that's done it helps you and makes you more productive.


Affine types are still too hard to use by regular users without CS background.

Simple exercise, imagine a GUI builder regardless of for web or native code, for pumping out plain CRUD frontends, dragging components from a toolbox into the UI designer.

Components, of course, have a property designer that allows setting properties like database connections or data grid paging.

Now try to fit affine types into this kind of workflow, without forcing people to manually create some kind of auxiliary types and deduction helper functions like described on the Idris book.


I don't see what you want to explain. Could you be a bit more specific ? In the use-case you describe, what don't you think could be done with a language with affine type system ?


Because the editor needs to update the type relations according to your mouse clicks and property editor changes, given the semantics of affine types I only see it being possible with some kind of heuristics.

Very basic example,

1 - Drag component into the form, variable could be declared as owned

2 - Edit a relationship between the component and another one on the property editor, either generate code with borrows or update the owned declaration into a Rc one, Arc instead, or Rc with RefCell, Arc with RefCell,....?

The GUI editor needs heuristics to create declarations with the right set of affine types, or to update existing ones when the developer clicks around changing the document tree and relations among them.

Oh and the current state of the borrow checker in Rust doesn't handle closures for self in callbacks, which makes it a royal pain to write GUI event handlers.


> The GUI editor needs heuristics to create declarations

How about using move by default, if the compiler complains about moving the data, falls back to Rc<RefCell<T>>, and if the compiler complains about `Send` use Arc<Mutex<T>>. Or you could even use the later for everything in your UI designer, this way you get a memory management scheme which is like Swift's. (In first approximation at least, since Swift has CoW for some kind of things, but you could definitely copy the exact Swift behavior in Rust if you wanted).

As I said earlier, the affine type system is opt-out in Rust, so you can do whatever you need without it(including your GUI stuff) and still be able to benefit from it when writing your business logic, and being more productive thanks to it.


I disagree. :) I have experience in low level programming and several MLs (and lots of misc other languages), so I don't think my opinions are shaped solely by familiarity.

There is a lot to like about Rust, but here are some things that I think make rust hard (and I think the difficulty of these features outweigh any productivity granted by the safety features).

First, the affine type system--I don't understand the use case for pushing ownership deeper into the stack by default, and it seems crazy that I have to know how all instances of a given type will be passed at definition time. It doesn't seem to facilitate safety, or at least it seems like the most flat-footed approach to preventing double-frees (never let two functions access the same memory, even those that can be trivially proven to run sequentially). I literally never want to move--I always want to shallow copy (or reference). Further, you can't call most pass-by-value functions from inside a pass by reference function because the former usually wants to destroy the value. Copy works around this in the simplest cases, but for the most part you end up with two categories of classes that can't be mixed. This is especially bad for open traits, where the trait definition has to decide how all implementations will pass and receive their arguments.

Beyond that, when defining a struct type that holds a reference, you have to know at definition time the lifetime of that reference, even it may vary from instance to instance (sometimes I want the struct to own the reference, but sometimes I don't). Maybe there's a way to express this in Rust, but it seems like the default is to assume that lifetimes are part of the type. This creates another dichotomy in types: those that own their data and references to data (String vs STR, vec vs slice, etc).

I like Rust. I love that it's an ML with a sane syntax, or a C-family language with decent functional features and sum types. I just can't justify these impediments. :(


> it seems like the most flat-footed approach to preventing double-frees (never let two functions access the same memory, even those that can be trivially proven to run sequentially)

This might just be a case of needing non-lexical lifetimes. I'm not sure about that, because I don't know the details of what issues you ran into.

> Beyond that, when defining a struct type that holds a reference, you have to know at definition time the lifetime of that reference, even it may vary from instance to instance (sometimes I want the struct to own the reference, but sometimes I don't).

The main issue I run into is when I want a struct to own a value, and for that struct to also include a reference into that value.


> This might just be a case of needing non-lexical lifetimes. I'm not sure about that, because I don't know the details of what issues you ran into.

I don't think the issue is about lexical lifetimes, but I don't have a very good understanding of what that means, either. My issue is that I frequently want to take some data and pass it into two separate functions, but I can't do this unless those functions are designed to take the value by reference. Even if those values contain owned references into the heap, the programs can be trivially proven to be correct. I can work around this by always passing references, but it seems strange to use reference semantics when all I really want is to not destroy the original value.

Another common case is that I have a borrowed reference that points to data I would like to pass (by value) into another function: `fn foo(bar: &Bar) { baz(bar.x) }`.


> Even if those values contain owned references into the heap, the programs can be trivially proven to be correct.

I don't think that's true. Passing data to a function when it's not a reference means that you're giving ownership to that function. How could you then pass it to another function (unless the first returned it again)? Once a function has ownership, it means nothing else has ownership of the data.

Basically, if you write a function that just takes a value, and doesn't take a reference, then you're telling your callers to give you ownership. If you don't want to take ownership, then just always accept a reference.

Maybe I'm misunderstanding the problem you're having. Do you have an example of another language that lets you do what you want?


> Passing data to a function when it's not a reference means that you're giving ownership to that function.

Only in Rust, but there's no apparent reason to move ownership. If Rust didn't move ownership during pass-by-value, it would still be trivial to prove the following code correct:

    fn main() {
        let x = String::new();
        foo(x);
        bar(x);
        // x is destroyed
    }
> Maybe I'm misunderstanding the problem you're having. Do you have an example of another language that lets you do what you want?

Here's the equivalent in C++:

   int main() {
       std::string x = "";
       foo(x);
       bar(x);
       // x is destroyed
   }


> Here's the equivalent in C++: >

   int main() {
       std::string x = "";
       foo(x);
       bar(x);
       // x is destroyed
   }
Doesn't C++ do a copy in this case?


It does; that's the point: you can copy without losing safety guarantees, so there's no obvious reason to use an affine type system.


You can copy in Rust too. You just call .clone(), or if you want your type to always be copied, have it implement the Copy trait. In that case, it does exactly what you want.

But Rust is more explicit about copying in general, to make it harder for the performance cost to be hidden.


If those two functions both need to own the data you're passing in, then they should take it by value, because in Rust passing by value means giving ownership of the data to the thing you're passing it to. It might give it back (as a return value), but that's another matter.

If a function only needs to use some data for a bit, it should borrow it i.e. pass by reference.

And if you need to give ownership of some data to a function, and you also need that data for something else, you have to copy it, so that you can give one copy to a function to do what it wants with, and keep the other one for your own purposes.


Well anyone is free to think anything, the Rust compiler is by nature very conservative and tries to take an approach where it wants to you put a lot of information so that it is easier for it compile programs and infer right context. In the process it demands a lot from you compared to Go. Being a C fan i would say C is way easier than Rust, but look where C has lead us, huge amount of headache due to tons of unspecified behavior of C which all compiler makers have diligently copied from each other. So i totally get it Rust is great for these scenarios absolutely, write OS Kernel, Databases, Browsers what have you... But it is really not meant for the web, trying to use Rust for web is retrofitting at best.


I'm using Rust/Rocket for Web apps and I'm as productive with that stack as I would be with, say, Go or Python/Flask. I'm more productive with Rust than I would be in Java. Programmers generally write the same amount of code (measured in LOC) per day regardless of the language, and Rust is pretty succinct. That said, I suspect if you want the absolute fastest development time, Rails and full-stack frameworks like it are still king.

In my case I'm writing a Web app to expose the functionality of a library that's written in Rust, and Rocket was a natural choice for that—it would make little sense to use some other language and go over the FFI. On the client side, I've found that TypeScript 2.x in strict mode goes nicely with Rust on the server, as both Rust and TS 2.x have comparably feature-rich and expressive type systems (generics, tuples, discriminated unions, a distinct null/option type, etc.)


What makes you think software developers write the same average amount of lines of code per day ? It seems to me the higher level the language is, higher is the average number of loc per day. IHMO Debugging has an huge impact on this average and lower level languages are harder to debug.


Why do you say that lower level languages are harder to debug? I'd think it's the opposite.


I have not found Rust any harder to debug than, say, Python. LLDB works fine.


I have no experience with rust so I can not elaborate on that, but I find harder to debug a multi-threaded C++ application where race condition and memory corruption is possible than a python one. That difficulty has an impact on average daily productivity on LOC.


That may be because you're programming C++. :)

As I recall, there is another bit of research that claims that the number of bugs written in all languages is about the same, except C++ which produces way more.


Rust web and database drivers still need a bit of maturing to be as productive as I can be with Spring, JEE and ASP.NET, Oracle and SQL Server, with multi-tier cluster based applications.


To be fair, you might be a little bit biased ;)


The compile time guarantees that Rust provides are good for web server code not a web app. This responsibly should and must be shifted to the people writing web server code not web applications. There are many nuances with rust and it is a hard language to use and learn, when libraries which you want to use dont compile, or your code than doesn't compile for 100 reasons. I get it, it is a great language, but only for Computer Scientists. For average WebDevelopers it may not be that useful to write web apps with compile time guarantees on memory safety.


Speaking as an engineer who (maybe surprisingly) isn't particularly interested in type theory for its own sake, I like the fact that Rust has an expressive type/macro system and an ecosystem that takes advantage of it, yet compiles to native binaries. The low-level memory management isn't much of a problem for me since I've internalized the rules, and the same system prevents nasty problems like data races.

Of course, different tools work well for different projects and different people's styles. If you needed me to write a CRUD Web app as quickly as possible I'd probably use Rails. After learning Rust and working with Rocket, though, I think most programmers will find the development experience enjoyable for lots of Web apps. I was pleasantly surprised at how elegant Rocket is.


See the post is about using Rust on the frontend. In my opinion it is not a suitable place for Go or Rust due to their large runtimes that needs to be transpiled. Even with webassembly the Runtime size would be huge. So it is not suited for the frontend. As far as backend is concerned the decreasing order of difficulty to write the same code seems something like this to me Rust -> Haskell -> Go -> Ruby -> Clojure.


Rust does not have a large runtime.


Haskell has more concepts than Rust (like higher kinder types) and should get linear types soon. Rust seems like Go with some Java and the great borrow checking system.


With the best will in the world, linear types are still going to take some serious work. Look at the guy doing a linear type streaming library.

And that's just at the language level. You then need pervasive library support. And then you need more GHC work on optimisation, work out how you interoperate with levity polymorphism and so on and so on.

And _after_ all this work is done, I seriously doubt you'll be able to control memory usage as well as Rust does now.

I love Haskell to bits, but there's no way it's going to beat Rust at what Rust's good at for a long time to come.


> And _after_ all this work is done, I seriously doubt you'll be able to control memory usage as well as Rust does now.

Which might be totally irrelevant in most use cases, if it is within the desired application bounds.

I have replaced more C++ systems with Java and .NET solutions than the other way around.


Well yes, but that's true right now as well. There's definitely things that Haskell's better than Rust at, but OP was arguing that it was going to be better in _all_ cases. Instead, the choice in five years time is going to be pretty similar to what it is now.


> but OP was arguing that it was going to be better in _all_ cases.

I wasn't clear to me if haskellandchill was claiming that Haskell will be better, or if Haskell is more complicated. I assumed they meant it's more complicated than Rust, but now I'm not sure.


Ironically, I think linear types are really cool. But I also think it's going to be years before benefits are seen in average programs.


I meant more complicated.


Associated types put Rust beyond Java and far beyond Go in terms of type system features, even without counting anything lifetime-related.


Interesting, didn't know it had something like type families. Thanks!


Higher kinded types don't seem like much of an extra concept. If anything they're a simplification: type parameters can be any type, rather than only being unparameterized types.


No, Rust is simple enough, just few first steps are difficult.


Rust is pretty much runtime-less. There's some unwinding stuff you could see as a runtime. You can simply replace it with a compiler flag. There is a standard library which you might mean, but you can opt out of that as well.


I built https://dtmf.io/ using Rust. The first prototype was using the Iron framework but then after fighting some parts of Iron, I pared it back to just using async hyper & handlebars-rs directly for the HTTP and templating. The glue to put them together is really minimal. Overall, I've been really impressed with Rust.


Nice overview, it might be worth mentioning that Emscripten isn't a tier 1 platform[1]. We've seen some asserts thrown in LLVM when building debug but go away when build release.

I've been trying to isolate it down to a reproducible sample but haven't nailed down exactly what's causing it yet.

Even with that issue it's been great to work with. I find myself so much more productive with Rust compared to C/C++.

[1] https://forge.rust-lang.org/platform-support.html


If you've got something large that asserts consistently, you could consider throwing some CPU time at it via creduce. It is designed for C, but Rust is close enough that a lot of its heuristics still work just fine.

Alternatively, since setting up/running creduce can be bit fiddly, if the code is open source, I suspect that someone would do it for you if you filed a bug against Rust pointing to an exact commit that demonstrates the problem with `cargo build --target=...` (or xargo) and requested/suggested creduce.


Thanks, I'll give that a shot. Unfortunately the code which is why I was trying to pair it down to a simple sample.


> NamedFile::open(Path::new("www/").join(file)).ok()

Is this vulnerable to the classic "../../../../../../../etc/passwd"?


I was thinking the exact same thing. Rust being a more memory-safe language does not mean it is a secure one. Still needs proper input validation.

EDIT: I opened the documentation and found https://api.rocket.rs/rocket/request/trait.FromSegments.html I don't fully understand if the checking they do on '..' fixes the attack vector here.


Slashes aren't allowed, so wouldn't be able to do any path traversals: https://api.rocket.rs/src/rocket/request/param.rs.html#298


> On Windows, decoded segment contains any of: '\'

And on Japanese and Korean windows? It uses the yen symbol as path separator. Depending on how the path is read or interpreted, filtering the yen may be necessary.

https://msdn.microsoft.com/en-us/library/dd374047(v=vs.85).a...


Afaik remember windows will parse additional dots beyond the two first as another level up? Cross-platform secure coding is hard... (Not sure about this code though, just making an observation).


Based on a quick read of the source, the conversion from a "<param..>" input to a "PathBuf" type strips leading ".." components automatically.


So would this still work?

  ValidDir/../../../../../../../etc/passwd


FYI, this is called a path traversal attack.


Just a small nitpick, but #3 is not actually an isomorphic app, it's just a regular server-rendered one. Isomorphic typically means that the app is rendered in the same way (e.g. via React) on both the frontend and backend, and the frontend degrades gracefully when JS isn't enabled.

More on-topic: I just recently started a new web app using Rust on the backend. Though there is definitely a lot still missing, it's amazing how far the language and its ecosystem have come in the past year or so. I expect that within a couple years, Rust will be a viable alternative to Python and Ruby for backend web development.


> I expect that within a couple years, Rust will be a viable alternative to Python and Ruby for backend web development

I like Rust, but I don't see this happening. Python, Ruby and PHP/Node.js languages became popular (and thus useful) because their learning curve is not steep - which is not something one can say about Rust. I consider myself capable developer but I struggled with many concepts. It's true that I didn't use it for a real project though, it might have been easier if I had clear goal in my mind.


That's true, and I don't mean to imply that I think it will replace Python/Ruby/etc. But I think Rust may find a sizeable niche as a high-performance, lower-level backend programming language for the web, sort of like Elixir. I've personally found the learning curve to be a lot smaller for web development than other projects because you don't have to learn everything at once; web development doesn't typically call for things like manual threading, so the borrow checker and other advanced features don't come up nearly as much or as early as they might for other codebases.


It _is_ isomorphic since he is shipping the rust code to the browser using web assembly.


It could be, hypothetically, but the description and example given don't do that.


See the first section on 'stdweb', and check the Github repo linked - the code is all there for DOM manipulation in the browser.

EDIT: what is going on here with the downvotes? Care to explain?


The author's "isomorphic" application is only stated as being the third section, where they're just rendering templates on the server side, and there's no client-side Rust at all.


It's cool that you can do this, but it's probably not something you want to do unless you have an unusual application. A reasonable application might be a heavily used API at the HTTP level, where the web-facing part is really a subroutine call interface, performance may be a big issue, and safety against bad parameters is a big issue.


You are cool, and it's all is amazing work. I bet it was very interesting to implement. But reading comments I think people don't understand that it's just demonstration of how much Rust evolved, not a tutorial of what they should do to use Rust for the Web. Kind of overkill :)


The 'Client-sode JS in Rust' part is a promising showcase for WebAssembly and I hope to see more languages compiling to WebAssembly soon. But looking closer at the code which does not a lot compared to its size I rather prefer the JS version.


I do believe big change is coming over the next few years, as we can develop reliable solutions on Web Assembly we're going to be able to use better languages.

It's going to be glorious!


In practice, I don't think Rust is really particularly well suited for this use case (especially the frontend stuff). But this is a really cool demo nonetheless, and the underlying WebAssembly will be something a lot of languages can use someday.


Yeah, the only convenience in this approach is to give you an ability to write JS right inside your Rust code. Somebody already starts implementing VIrtualDOM and something so we can expect more in the future.


This makes no mention of the size of these applications. Given the recent focus on shipping less JavaScript to the browser to better support mobile and slow networks, I wonder how feasible this project even is?


Well, that focus is if you are looking to market your apps in those markets. If your market has ubiquitous fast affordable internet, shipping more javascript is not that much of an issue, specially if it makes your web application faster and easier to run or develop or provides other benefits.


I guess unless the author made size reduction a priority it doesn't really make too much sense to judge the project on those characteristics. It seems like an interesting path.


I can't help thinking something like Wt, but in Rust, would be an awfully handy way to write webapps.


Nice blog! Do you have a RSS feed? Can't find any...


This is all good and dandy but I don't like the webpage font-color! Can we have more of black on black so that it's even more modern and minimal? Or white-on-white which is more of a trend these days?


+1 for the snark, however I think the site looks good and the contrast is just right.

there is an official Chrome plugin that lets you meddle with a site's contrast these days if you're having trouble (like me) with this awful trend that's approaching white-on-white: https://chrome.google.com/webstore/detail/high-contrast/djcf...


;(


Lol, I'm sorry it was a bit snarky! You've obviously done a great job but if anything the jab was not at you but at the trend...


Rust is cool. But I don't think it's good for web backend at this time. I will try it when working with WebAssembly.


I don't understand why you are downvoted : that's absolutely true, Rust isn't quite the best choice for web backend ATM, by far.

It's way better than last summer though, rocket and the recently announced gotham framework look great, and I'm really excited to see how it evolves in the next few months.




Applications are open for YC Summer 2020

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

Search: