Embeddable / Interoperable with ruby

Let’s take this code:

def foo(x)
  if x.is_a?(Int32)
    1
  else
    2
  end
end

foo(1 || 'a')
foo('a' || 1)

We can compile it like this:

crystal build --prelude=empty --emit llvm-ir foo.cr

That will give us the LLVM IR for that file, and specifically for what the generated foo function is.

How does the generated code check if x.is_a?(Int32)? The answer is that Crystal gives each type a unique ID: an integer.

Here’s the LLVM IR code for the generated foo function:

define internal i32 @"*foo<(Char | Int32)>:Int32"(%"(Char | Int32)" %x) #0 !dbg !21 {
alloca:
  %x1 = alloca %"(Char | Int32)", align 8, !dbg !22
  br label %entry

entry:                                            ; preds = %alloca
  store %"(Char | Int32)" %x, %"(Char | Int32)"* %x1, align 8, !dbg !22
  %0 = getelementptr inbounds %"(Char | Int32)", %"(Char | Int32)"* %x1, i32 0, i32 0, !dbg !23
  %1 = load i32, i32* %0, align 4, !dbg !23
  %2 = getelementptr inbounds %"(Char | Int32)", %"(Char | Int32)"* %x1, i32 0, i32 1, !dbg !23
  %3 = icmp eq i32 12, %1, !dbg !23
  br i1 %3, label %then, label %else, !dbg !23

then:                                             ; preds = %entry
  br label %exit, !dbg !23

else:                                             ; preds = %entry
  br label %exit, !dbg !23

exit:                                             ; preds = %else, %then
  %4 = phi i32 [ 1, %then ], [ 2, %else ], !dbg !23
  ret i32 %4, !dbg !23
}

It’s hard to grok, but there are these lines:

  %3 = icmp eq i32 11, %1, !dbg !23
  br i1 %3, label %then, label %else, !dbg !23

If “something” is 11, then jump to the “then” label, otherwise jump to the “else” label. That 11 is the ID Crystal assigned to the Int32 type.

Now let’s slightly change the original program:

# This type wasn't there before!
class Foo
end

def foo(x)
  if x.is_a?(Int32)
    1
  else
    2
  end
end

foo(1 || 'a')
foo('a' || 1)

Recompiling and checking the generated LLVM IR, we get this now for foo:

define internal i32 @"*foo<(Char | Int32)>:Int32"(%"(Char | Int32)" %x) #0 !dbg !21 {
alloca:
  %x1 = alloca %"(Char | Int32)", align 8, !dbg !22
  br label %entry

entry:                                            ; preds = %alloca
  store %"(Char | Int32)" %x, %"(Char | Int32)"* %x1, align 8, !dbg !22
  %0 = getelementptr inbounds %"(Char | Int32)", %"(Char | Int32)"* %x1, i32 0, i32 0, !dbg !23
  %1 = load i32, i32* %0, align 4, !dbg !23
  %2 = getelementptr inbounds %"(Char | Int32)", %"(Char | Int32)"* %x1, i32 0, i32 1, !dbg !23
  %3 = icmp eq i32 12, %1, !dbg !23
  br i1 %3, label %then, label %else, !dbg !23

then:                                             ; preds = %entry
  br label %exit, !dbg !23

else:                                             ; preds = %entry
  br label %exit, !dbg !23

exit:                                             ; preds = %else, %then
  %4 = phi i32 [ 1, %then ], [ 2, %else ], !dbg !23
  ret i32 %4, !dbg !23
}

Do you see what the comparison looks now?

  %3 = icmp eq i32 12, %1, !dbg !23
  br i1 %3, label %then, label %else, !dbg !23

Now the compiler assigned 12 to the type ID of Int32, presumably because types were ordered alphabetically and Foo comes before Int32. Well, I’m not sure if that’s the reason, but what’s important to know is that the type ID of a given type isn’t guaranteed to be the same across different compilations.

If we compile two Crystal programs into shared libraries and load them both at the same time, one of the foo will win and override the other. When doing that, the logic for one of the programs will break, because if we pass an Int32 in that program the check x.is_a?(Int32) will be false, likely leading to a segfault.

This is the main reason creating shared libraries in Crystal isn’t possible right now. Well, it’s possible if you only use one shared library, and that’s it… which in my opinion is not very useful.

So if you run into segfaults or strange behavior when playing with this… you know why! You didn’t do anything wrong: it’s just not supposed to work at all.

If we want this to work, we first have to solve the problem of changing type IDs.

6 Likes