Crystal: Scripting language? C glue? Both!

This is going to be brief. I’ve been learning a little rails for a yet to be released post. But I’ve noticed in a few forums that a language called Crystal was popping up. Wanting to play around a little bit I installed it and started writing code. The DX is pretty horrid at the moment. Expect very little in the way of editor support. More on that later. But let’s look at the two things that shockingly just worked. Crystal claims to be just like ruby and to have great support for c. So let’s try both out. I wanted to target linenoise because it’s a library with a tiny footprint.

Scripting with Crystal

First things first, let’s create a little script to download linenoise and compile it. I wanted it to be tiny, straightforward, and just work. Here’s the script:

require "http"
require "file_utils"

def get_follow(url)
    response = HTTP::Client.get url
    while response.headers.has_key? "Location"
        response = HTTP::Client.get response.headers["Location"]
    end
    response
end
# Fetch the linenoise header and c file
header_res = get_follow "https://github.com/antirez/linenoise/raw/master/linenoise.h"
c_res = get_follow "https://github.com/antirez/linenoise/raw/master/linenoise.c"

FileUtils.rm_r "lib/linenoise"
FileUtils.mkdir_p "lib/linenoise"
File.write "lib/linenoise/linenoise.h", header_res.body
File.write "lib/linenoise/linenoise.c", c_res.body

# Let's build the linenoise.o file
`cc -c -o lib/linenoise/linenoise.o lib/linenoise/linenoise.c`

if $?.success?
  puts "Successfully built linenoise!"
else
  puts "Failed to build linenoise!"
end

I think it worked on the second try. Not bad right? Looks like any other modern dynamic language. Except that compiles down to a single binary. Not very useful in this case but I think you’d be hard pressed to find a language that can do this more expressively. Even bash with curl and everything else wouldn’t be much smaller and anything even slightly more complex with conditionals would be a nightmare.

Interfacing with C

But now let’s look at actually using the little linenoise.o file we just compiled. Here’s a simple program that uses it:


@[Link(ldflags: "#{__DIR__}/../lib/linenoise/linenoise.o")]
lib RawLinenoise
    fun linenoise(prompt : LibC::Char*) : LibC::Char*
end

def extract_string(pointer : LibC::Char*) : String
    val = String.new(pointer)
    LibC.free(pointer)
    val
end

module Linenoise
    def self.linenoise(prompt : String) : String
        extract_string RawLinenoise.linenoise(prompt)
    end
end


while true
    input = Linenoise.linenoise("> ")
    break if input.nil? || input.empty?
    puts "You entered: #{input}"
end

Does that not blow your mind a little? Like I just compiled a c file fetched from git, and referenced it in like 60-ish lines of code. You could get it down even smaller.

Conclusion

Go take a look at the standard library to get an idea of the batteries included nature. It feels a lot like ruby from what little knowledge I have of either framework or ecosystem but I am very certain that this language will come back to glue some bits together for me at some point. It’s hard to find much of a use case other than that at the moment, but it’s certainly an interesting language.

Oh, and if you want to up your developer experience, other than installing the language server for VS code there’s one other thing that helped a lot. Install watchexec, open up your tasks.json and add this task:

{
    "label": "Crystal spec watch",
    "type": "shell",
    "command": "watchexec -e cr -d 1 crystal spec",
    "isBackground": true,
    "problemMatcher": {
        "background": {
            "activeOnStart": false,
            "beginsPattern": "^\\[Running.*",
            "endsPattern": "^\\[Command.*",
        }
    }
}

It’s not perfect, but the errors are usually pretty useful and it works to get you a little closer to the feedback loop you’re used to in other languages.