Exporting a function as a class/instance function

I’m writing some c++ code, but want to implement some functions in Crystal and link everything together. For top-level functions this works just fine:

// cc.cc
extern "C" {    
  int foo(int);  //I'll write this one in Crystal 
  
  extern void bla(){
    printf("%i\n", foo(7));
  }
}
# crystal.cr
@[Link(ldflags: "#{__DIR__}/cc.cc --std=c++2a")]

fun foo(x : LibC::Int) : LibC::Int
  x*2                               # glad I didn't have to write this in c++
end

But let’s say, I have some c++ classes and would like to write some of their functions also in Crystal - the c++ part would look like this I guess:

// cc.cc
extern "C" {    
  class Foo {
    int foo(int);            // I'll write this one in Crystal 
    static int bar (int);    // and this one, too
  }

  extern void bla(){
    printf("%i\n", Foo::bar(7));
  }
}

But I’m clueless on how the Crystal code would have to look like:

fun Foo.bar(LibC::Int) : LibC::Int                # that's wrong
end

fun qwerty = "Foo.bar"(LibC::Int) : LibC::Int     # that's wrong
end

fun qwerty = "Foo::bar"(LibC::Int) : LibC::Int    # that's wrong
end

I also played around with @[Extern] and lib or by naming things ‘conveniently’ (but even when I managed nm to output the same symbol names for both object files, I just couldn’t get it to link).

Is it possible like this? Or is the only way around by using some additional (top-level) helper functions?

Thanks in advance!

1 Like

I think C++ name mangling is not standard, so there’s no predicatable name you can find for Foo.foo or Foo.bar :thinking:

1 Like

As a side node, exporting functions from Crystal to other programs will work for a tiny bit. If you then do this multiple times (say you compile multiple Crystal libs to export functions from them) then it will crash badly.

My advise is to use another language for this. For example Rust.

1 Like

Carbon claims to be interoperable with C++, which might be what you want.

Also note that even if you could figure out the mangled name, there might still be issues with the call convention; the compiler most likely doesn’t support thiscall on Windows.

1 Like

Better create a C wrapper for your C++ class and implement this in Crystal, so you run away from C++ name mangling and calling conventions (sort of), maybe using the GC in the C++ code would also reduce the pain.

class Foo;

extern "C" { 
  int foo_foo(Foo* self, int i);
}

class Foo {
  int foo(int i);
};

int Foo::foo(int i) {
  return foo_foo(this, i);
}

BTW, If I remember correctly declaring a C++ class inside a extern "C" block is invalid, but I’m not sure… long time I don’t work with C++.

But… if even so you want to know the C++ name used internally, use the nm tool in the object file or shared library to see what weird and long string was used for the instance method name.

For this code the foo.so library compiled on Linux with gcc get the symbols:

0000000000004010 b completed.0
                 w __cxa_finalize@GLIBC_2.2.5
0000000000001040 t deregister_tm_clones
00000000000010b0 t __do_global_dtors_aux
0000000000003dd0 d __do_global_dtors_aux_fini_array_entry
0000000000004008 d __dso_handle
0000000000003dd8 d _DYNAMIC
000000000000112c t _fini
                 U foo_foo
0000000000001100 t frame_dummy
0000000000003dc8 d __frame_dummy_init_array_entry
0000000000002080 r __FRAME_END__
0000000000003fe8 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
0000000000002000 r __GNU_EH_FRAME_HDR
0000000000001000 t _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000001070 t register_tm_clones
0000000000004010 d __TMC_END__
000000000000110a T _ZN3Foo3fooEi

If you call nm with -C to demangle the names you find what you are looking for:

0000000000004010 b completed.0
                 w __cxa_finalize@GLIBC_2.2.5
0000000000001040 t deregister_tm_clones
00000000000010b0 t __do_global_dtors_aux
0000000000003dd0 d __do_global_dtors_aux_fini_array_entry
0000000000004008 d __dso_handle
0000000000003dd8 d _DYNAMIC
000000000000112c t _fini
                 U foo_foo
0000000000001100 t frame_dummy
0000000000003dc8 d __frame_dummy_init_array_entry
0000000000002080 r __FRAME_END__
0000000000003fe8 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
0000000000002000 r __GNU_EH_FRAME_HDR
0000000000001000 t _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000001070 t register_tm_clones
0000000000004010 d __TMC_END__
000000000000110a T Foo::foo(int)

Foo::foo(int) is mangled as _ZN3Foo3fooEi, so this ugly name should work on Crystal… until someone try to compile using another compiler/platform :sweat_smile:

Ah, you probably need to worry about symbol visibility too if you plan to use shared libraries… I also don’t remember the details of globals initialization in C++ shared libraries, but all this have a list of caveats as well…

As a side note, as it wasn’t visible in my post: main is used from Crystal (the C++ code doesn’t provide one). So I start in Crystal, run primarily Crystal, but occasionally call C++, which sometimes would call Crystal again. This works (without any trickery/ modification/ hacks needed), except for Crystal class/instance methods because of the naming.

Thanks for mentioning, but I really would like to do the main part just in Crystal. :slightly_smiling_face:

It actually is wrapped (even in the example). And I checked via nm (but to my somewhat surprise: even when the names eventually matched the linker would complain about the symbols missing; I didn’t look deeper into it, as I achieved the matching names only by insane names in the first place out of curiosity))

It’s solved. As soon as the mangled name is used as a string, it works just fine.
For anyone who wants to do something similar and is wondering about the syntax of the mangled name: as long as the exporting compiler wasn’t MSVC, the chances are good that it follows this syntax (which looks far worse than it actually is)

2 Likes