Crystal: Scripting language? C glue? Both!
3/8/2025This 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.
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.
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.
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.