No need to be impressed - I’m really not doing much. If one starts the whole thing still via crystal code, then it’s actually not even something which isn’t offered by crystal anyway and could only be seen as syntactic sugar (if even).
As I said yesterday I would explain it all with some code, here it comes.
So the following works fine for me (on osx 10.14 and some recent alpine linux):
Let’s start on the C side with some very standard code:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[]){
return 0;
}
And let’s say we would really like to use two functions from Crystal, because they are really tricky to implement, so we would prefer to make life easier and more enjoyable by writing them in Crystal. Let’s call those functions foo
and throwDice
.
def foo(i)
puts "foo!"
(1..i).each do |j|
puts "foo #{j}"
end
puts "foo!!!"
end
def throwDice
1_u8 + Random::Secure.rand(6)
end
(now that you have seen foo
you understand why I didn’t want to write that in C )
Still absolutely to the standard so far. Same goes for having to tell the C code about them in order to call them. And let’s also directly use them in C:
#include <stdio.h>
#include <stdlib.h>
void foo(int);
char throwDice();
int main(int argc, char* argv[]){
foo(10);
printf("Alea iacta est: %i!\n", throwDice());
return 0;
}
From C’s point of view, the only thing that’s missing is the implementation of the two functions (which would be normally maybe anyway stored in other files, something like foo.c
and gambling.c
). So still nothing unusual.
If you would try to compile the code already, the linker would be complaining about foo
and throwDicing
being referenced but missing.
Our Crystal code has those two functions, so let’s just put things together.
If you would try to link the object files of Crystal with the one of C, among other issues the linker would still complain about those two methods being missing. You first have to make them visible or whatever (I actually haven’t even checked why their symbols aren’t there). Anyway, you have to make some small changes to your Crystal code and that’s where the fun begins (pun intended) change the def
s to fun
s of all your Crystal functions you want to use in C. And while you are at it, set the types the usual lib
-fun
way, as if it were for the other direction.
fun foo(i : LibC::Int) : Void
puts "foo!"
(1..i).each do |j|
puts "foo #{j}"
end
puts "foo!!!"
end
fun throwDice() : UInt8
1_u8 + Random::Secure.rand(6)
end
Still no big act (and I believe we still move within what’s intended by Crystal, but I’m not 100% sure).
If you would now again compile everything to object files and try to link them, it would still not work, but at least the symbols of the two functions would be no longer missing - that part is done. But as I said, there is still some issue, we have to main
functions and the linker doesn’t like duplicates. So let’s rename the one of C simply to otherMain
. If you would re-compile and try again to link, it would actually work. Or sort of. You receive your executable, but when you start it nothing happens, not even an error, and it just ends immediately (at least successfully tho).
And that’s kinda absolutely what we would have to expect, as there is nothing going on in our Crystal code, it starts, there is nothing to do and it ends. otherMain
would actually work but it never gets called. But we wanted it to run the C code after all, or we wouldn’t have bothered about using crystal code in C in the first place.
It’s easy so. Just add to your crystal code that you want to run otherMain. And that’s actually all we want Crystal to run here. As Crystal wouldn’t know of otherMain
, just do it like with any other external stuff you would want to run in Crystal. With the Link annotation, we can be cheeky and even give it our source (c.c
here) instead of the usual library name (you don’t HAVE to do that. This is really optional. But if you do, you can later just do a crystal build myCrystalFile.cr
(as well as --static
/--release
/etc as you wish) and all will be done at once; if your c code requires more files or any flags, jsut add them within the string of the annotation). And we would really like everything to behave as if it was all C we started, so we also take care of the exit code and ARGV. And our Crystal code then looks like this:
@[Link(ldflags:"#{__DIR__}/c.c")]
lib LibOther
fun otherMain(LibC::Int, UInt8**) : LibC::Int
end
fun foo(i : LibC::Int) : Void
puts "foo!"
(1..i).each do |j|
puts "foo #{j}"
end
puts "foo!!!"
end
fun throwDice() : UInt8
1_u8 + Random::Secure.rand(6)
end
exit LibOther.otherMain(ARGV.size, ARGV.map(&.to_unsafe))
And that’s it, nothing else left to be done. But feel free to add other exciting Crystal functions if foo
and throwDice
aren’t exciting enough for you. You should just stay within one Crystal application (as you would normally create one executable by calling the crystal command just once) - this one crystal application can contain dozens of unrelated tasks or shards tho.
Run your crystal build
command and enjoy. Your C code looks almost perfectly normal (only difference is the renamed main
, that’s it). Your Crystal code has a tiny bit of boilerplate, but is also still very much the way as it would usually look like. I’m happy with this solution and maybe you will be, too.
You could even go and build a static crystal library which you then link with whatever C project you might have (then you basically are really at the point of calling it your very standard library behaviour as one would expect). And on osx there is nothing else you would have to adjust, just as it is, go and do so (choose cross-compiling, and then use the Crystal object file like you would usually do in order to create your static dylib or bundle). On linux I wasn’t able to do so, but that’s probably just because I wouldn’t know how to create a static (working!) library including Crystal’s dependencies (-lz
-lpcre
etc) so that issue seems not necessarily related to the mixing of languages
Oh, and just in case: It doesn’t just work for such awesome things like foo
or a random number between 1 and 6. It works just the same with such trivial or boring things like Kemal or HTTP::Client