cannam@62: --- cannam@62: layout: post cannam@62: title: "Cap'n Proto v0.2: Compiler rewritten Haskell -> C++" cannam@62: author: kentonv cannam@62: --- cannam@62: cannam@62: Today I am releasing version 0.2 of Cap'n Proto. The most notable change: the compiler / code cannam@62: generator, which was previously written in Haskell, has been rewritten in C++11. There are a few cannam@62: other changes as well, but before I talk about those, let me try to calm the angry mob that is cannam@62: not doubt reaching for their pitchforks as we speak. There are a few reasons for this change, cannam@62: some practical, some ideological. I'll start with the practical. cannam@62: cannam@62: **The practical: Supporting dynamic languages** cannam@62: cannam@62: Say you are trying to implement Cap'n Proto in an interpreted language like Python. One of the big cannam@62: draws of such a language is that you can edit your code and then run it without an intervening cannam@62: compile step, allowing you to iterate faster. But if the Python Cap'n Proto implementation worked cannam@62: like the C++ one (or like Protobufs), you lose some of that: whenever you change your Cap'n Proto cannam@62: schema files, you must run a command to regenerate the Python code from them. That sucks. cannam@62: cannam@62: What you really want to do is parse the schemas at start-up -- the same time that the Python code cannam@62: itself is parsed. But writing a proper schema parser is harder than it looks; you really should cannam@62: reuse the existing implementation. If it is written in Haskell, that's going to be problematic. cannam@62: You either need to invoke the schema parser as a sub-process or you need to call Haskell code from cannam@62: Python via an FFI. Either approach is going to be a huge hack with lots of problems, not the least cannam@62: of which is having a runtime dependency on an entire platform that your end users may not otherwise cannam@62: want. cannam@62: cannam@62: But with the schema parser written in C++, things become much simpler. Python code calls into cannam@62: C/C++ all the time. Everyone already has the necessary libraries installed. There's no need to cannam@62: generate code, even; the parsed schema can be fed into the Cap'n Proto C++ runtime's dynamic API, cannam@62: and Python bindings can trivially be implemented on top of that in just a few hundred lines of cannam@62: code. Everyone wins. cannam@62: cannam@62: **The ideological: I'm an object-oriented programmer** cannam@62: cannam@62: I really wanted to like Haskell. I used to be a strong proponent of functional programming, and cannam@62: I actually once wrote a complete web server and CMS in a purely-functional toy language of my own cannam@62: creation. I love strong static typing, and I find a lot of the constructs in Haskell really cannam@62: powerful and beautiful. Even monads. _Especially_ monads. cannam@62: cannam@62: But when it comes down to it, I am an object-oriented programmer, and Haskell is not an cannam@62: object-oriented language. Yes, you can do object-oriented style if you want to, just like you cannam@62: can do objects in C. But it's just too painful. I want to write `object.methodName`, not cannam@62: `ModuleName.objectTypeMethodName object`. I want to be able to write lots of small classes that cannam@62: encapsulate complex functionality in simple interfaces -- _without_ having to place each one in cannam@62: a whole separate module and ending up with thousands of source files. I want to be able to build cannam@62: a list of objects of varying types that implement the same interface without having to re-invent cannam@62: virtual tables every time I do it (type classes don't quite solve the problem). cannam@62: cannam@62: And as it turns out, even aside from the lack of object-orientation, I don't actually like cannam@62: functional programming as much as I thought. Yes, writing my parser was super-easy (my first cannam@62: commit message was cannam@62: "[Day 1: Learn Haskell, write a parser](https://github.com/kentonv/capnproto/commit/6bb49ca775501a9b2c7306992fd0de53c5ee4e95)"). cannam@62: But everything beyond that seemed to require increasing amounts of brain bending. For instance, to cannam@62: actually encode a Cap'n Proto message, I couldn't just allocate a buffer of zeros and then go cannam@62: through each field and set its value. Instead, I had to compute all the field values first, sort cannam@62: them by position, then concatenate the results. cannam@62: cannam@62: Of course, I'm sure it's the case that if I spent years writing Haskell code, I'd eventually become cannam@62: as proficient with it as I am with C++. Perhaps I could un-learn object-oriented style and learn cannam@62: something else that works just as well or better. Basically, though, I decided that this was cannam@62: going to take a lot longer than it at first appeared, and that this wasn't a good use of my cannam@62: limited resources. So, I'm cutting my losses. cannam@62: cannam@62: I still think Haskell is a very interesting language, and if works for you, by all means, use it. cannam@62: I would love to see someone write at actual Cap'n Proto runtime implementation in Haskell. But cannam@62: the compiler is now C++. cannam@62: cannam@62: **Parser Combinators in C++** cannam@62: cannam@62: A side effect (so to speak) of the compiler rewrite is that Cap'n Proto's companion utility cannam@62: library, KJ, now includes a parser combinator framework based on C++11 templates and lambdas. cannam@62: Here's a sample: cannam@62: cannam@62: {% highlight c++ %} cannam@62: // Construct a parser that parses a number. cannam@62: auto number = transform( cannam@62: sequence( cannam@62: oneOrMore(charRange('0', '9')), cannam@62: optional(sequence( cannam@62: exactChar<'.'>(), cannam@62: many(charRange('0', '9'))))), cannam@62: [](Array whole, Maybe> maybeFraction) cannam@62: -> Number* { cannam@62: KJ_IF_MAYBE(fraction, maybeFraction) { cannam@62: return new RealNumber(whole, *fraction); cannam@62: } else { cannam@62: return new WholeNumber(whole); cannam@62: } cannam@62: }); cannam@62: {% endhighlight %} cannam@62: cannam@62: An interesting fact about the above code is that constructing the parser itself does not allocate cannam@62: anything on the heap. The variable `number` in this case ends up being one 96-byte flat object, cannam@62: most of which is composed of tables for character matching. The whole thing could even be cannam@62: declared `constexpr`... if the C++ standard allowed empty-capture lambdas to be `constexpr`, which cannam@62: unfortunately it doesn't (yet). cannam@62: cannam@62: Unfortunately, KJ is largely undocumented at the moment, since people who just want to use cannam@62: Cap'n Proto generally don't need to know about it. cannam@62: cannam@62: **Other New Features** cannam@62: cannam@62: There are a couple other notable changes in this release, aside from the compiler: cannam@62: cannam@62: * Cygwin has been added as a supported platform, meaning you can now use Cap'n Proto on Windows. cannam@62: I am considering supporting MinGW as well. Unfortunately, MSVC is unlikely to be supported any cannam@62: time soon as its C++11 support is cannam@62: [woefully lacking](http://blogs.msdn.com/b/somasegar/archive/2013/06/28/cpp-conformance-roadmap.aspx). cannam@62: cannam@62: * The new compiler binary -- now called `capnp` rather than `capnpc` -- is more of a multi-tool. cannam@62: It includes the ability to decode binary messages to text as a debugging aid. Type cannam@62: `capnp help decode` for more information. cannam@62: cannam@62: * The new [Orphan]({{ site.baseurl }}/cxx.html#orphans) class lets you detach objects from a cannam@62: message tree and re-attach them elsewhere. cannam@62: cannam@62: * Various contributors have declared their intentions to implement cannam@62: [Ruby](https://github.com/cstrahan/capnp-ruby), cannam@62: [Rust](https://github.com/dwrensha/capnproto-rust), C#, Java, Erlang, and Delphi bindings. These cannam@62: are still works in progress, but exciting nonetheless! cannam@62: cannam@62: **Backwards-compatibility Note** cannam@62: cannam@62: Cap'n Proto v0.2 contains an obscure wire format incompatibility with v0.1. If you are using cannam@62: unions containing multiple primitive-type fields of varying sizes, it's possible that the new cannam@62: compiler will position those fields differently. A work-around to get back to the old layout cannam@62: exists; if you believe you could be affected, please [send me](mailto:temporal@gmail.com) your cannam@62: schema and I'll tell you what to do. [Gory details.](https://groups.google.com/d/msg/capnproto/NIYbD0haP38/pH5LildInwIJ) cannam@62: cannam@62: **Road Map** cannam@62: cannam@62: v0.3 will come in a couple weeks and will include several new features and clean-ups that can now cannam@62: be implemented more easily given the new compiler. This will also hopefully be the first release cannam@62: that officially supports a language other than C++. cannam@62: cannam@62: The following release, v0.4, will hopefully be the first release implementing RPC. cannam@62: cannam@62: _PS. If you are wondering, compared to the Haskell version, the new compiler is about 50% more cannam@62: lines of code and about 4x faster. The speed increase should be taken with a grain of salt, cannam@62: though, as my Haskell code did all kinds of horribly slow things. The code size is, I think, not cannam@62: bad, considering that Haskell specializes in concision -- but, again, I'm sure a Haskell expert cannam@62: could have written shorter code._