Calling FFI C library with C++ internal implementation

I am trying to interface to a library which is written in C++ but which provides a C interface. My basic test has involved making a call which should trigger a callback (after some internal processing, including spawning hidden threads and connecting to an external service): my C test works, my rust equivalent does not generate the callback.

My best bet is that the library depends on static C++ constructors, which I understand rust does not trigger. I have tested the interface with both static and dynamic linking (using the libloading crate for the dynamically linked version), and I have also tested the library interface with Python and ctypes: in all cases everything works as expected for C and Python, but not with rust.

I was sort of hoping that dynamically loading the library would trigger the C++ constructors, but apparently not.

Can anyone offer any advice about the next step? I can post example code and links to the original library, but I guess my first questions are:

  1. Is my diagnosis that the C++ static constructors are not being called a plausible explanation?
  2. Is there a workaround, either for dynamic or static linking (either will suffice)?
  3. What's the next best step for me? This is my first foray into rust and this forum.

Quite plausible, given how "unreliable" the interaction between static initializers and linkers is. I've stumbled upon this blog post that witnesses issues even from within C++ itself, provided different compilation units are used (first one for the static library, then the binaray itself linking against it):

At the end of the day, it recommends that the C++ library export / offer some init()-like actual function, that is to be explicitely (and thus manually) called by the other side of the FFI (this is anyways the most portable solution); they call it the "brute force" solution.

  • If you are working on Linux, OSX or Windows, then you can even "hide" that explicit init() call using the ::ctor crate.

Hmm. I'm not really in a position to modify the library (well, that's not totally true, but I'd rather not go there). I don't think the ::ctor crate helps me here, does it?

I've read that rust specifically does not call constructors, but this is tending to tell me that I can't use this FFI interface. I remain quite perplexed that the dynamic loading solution also didn't work from rust, as it works just fine from Python using ctypes (haven't tried doing it "by hand" from C yet).

Indeed, ::ctor's purpose would be for your Rust crate not to have to call ffi::init() right when entering main(), and instead just have a #[ctor] unsafe fn call_ffi_init () { ffi::init() } thrown in there.

Yeah, I haven't yet linked against c++ that relied on static initializers, so I can't help you in that regard; I imagine there must be some linker flag that could be helpful in these situations1, so let's wait for someone else more versed in this topic to chime in.


1 You can try the linker flags mentioned in the post I linked to by creating a .cargo/config file with:

[build]
rust_flags = ["-C", 'link-args="..."']

# or:
[target.x86_64-unknown-linux-gnu]  # for some specific target
rust_flags = ["-C", "link-args=-whole-archive"]

but since linker flags are order sensitive, this may or may not work depending on where exactly does cargo choose to inject them.

On windows, the CRT is responsible for running all c++ constructors for static variables and it does so in the entry point. When you load a dll, its entry point is automatically invoked, and all the constructors initialized. At least for Windows this is unlikely to be the problem. Other platforms vary in how runtime initializers are handled.

1 Like

Soo... My theory is proved wrong: I created a simple C++ test library:

class MyClass
{  public:
    MyClass(const char *name) : name(name)
    {
        printf("Calling MyClass %s\n", name);
    }
    const char *name;
};

static MyClass instance("Static name");

and a simple rust using dlopen to open it:

use dlopen;
const TESTLIB_PATH: &str = "/home/mga83/ideas/rust/ca/libtestlib.so.1.0";
fn main()
{
    let _lib = dlopen::raw::Library::open(TESTLIB_PATH).unwrap();
    println!("Library opened");
}

and on running it I see the static constructor is called.

So much for that theory. Now I have no idea why my callback isn't triggering :frowning: Problem for another day, maybe another thread :grimacing:

May be you forgot mark callback function as extern "C"?

Wouldn't that fail at compile time? For what it's worth, here is my failing rust program (forgive the ugly ugly code, just throwing something together):

use libc::{c_char, c_int, c_uint, c_void};
use dlopen;

const LIB_CA_PATH : &str =
    "/dls_sw/epics/R3.14.12.7/base/lib/linux-x86_64/libca.so";

#[repr(C)]
#[allow(non_camel_case_types)]
#[allow(dead_code)]     // For unused variant
enum ca_preemptive_callback_select {
    ca_disable_preemptive_callback,
    ca_enable_preemptive_callback,
}

#[repr(C)]
struct ca_connection_handler_args {
    chid: *const ChanId,
    op: c_int,
}

#[repr(C)]
#[derive(Debug)]
struct oldChannelNotify { _unused: [u8; 0] }
type ChanId = *mut oldChannelNotify;


extern fn on_connect(_args: ca_connection_handler_args)
{
    println!("Hello there!");
}

fn main()
{
    let lib = dlopen::raw::Library::open(LIB_CA_PATH).unwrap();

    let ca_context_create :
        unsafe extern fn(select: ca_preemptive_callback_select) -> c_int =
            unsafe { lib.symbol("ca_context_create") }.unwrap();
    unsafe { ca_context_create(
        ca_preemptive_callback_select::ca_disable_preemptive_callback) };

    let ca_create_channel : unsafe extern fn (
        pv: *const c_char,
        on_connect : extern fn(args: ca_connection_handler_args),
        context: *const c_void,
        priority: c_uint,
        channel_id: *mut ChanId) -> c_int = unsafe {
            lib.symbol("ca_create_channel") }.unwrap();
    let ca_pend_event : unsafe extern fn (delay: f64) -> c_int =
        unsafe { lib.symbol("ca_pend_event") }.unwrap();

    let pv = "SR-DI-DCCT-01:SIGNAL";
    let cpv = std::ffi::CString::new(pv).unwrap().as_ptr();
    let mut chan_id = std::mem::MaybeUninit::zeroed();
    let rc = unsafe {
        ca_create_channel(
            cpv, on_connect, std::ptr::null(), 0, chan_id.as_mut_ptr()) };
    let chan_id = unsafe { chan_id.assume_init() };
    println!("ca_create_channel => {:} {:?}", rc, chan_id);

    unsafe { ca_pend_event(1.0) };
}

and here is the corresponding C code:

#include <stdio.h>
#include "cadef.h"

static void on_connect(struct connection_handler_args args)
{
    printf("Connected\n");
}

int main(int argc, char *argv[])
{
    const char *pv = "SR-DI-DCCT-01:SIGNAL";
    chid chanId;
    ca_context_create(ca_disable_preemptive_callback);
    int rc = ca_create_channel(pv, on_connect, NULL, 0, &chanId);
    printf("ca_create_channel => %d %p\n", rc, chanId);
    ca_pend_event(1.0);
    return 0;
}

In the latter on_connect is called, but not in the former.

It fail if you run bindgen again C header file (*.h) and that used generated Rust code . But you specify type of functions by hands, and looks like in the wrong way, missed extern "C", may be there are other errors.

does not match

try removing the *const there and see if it works (and replace c_int with c_long btw)


Also,

creates a dangling pointer: CString::new(pv) heap-allocates a copy of pv with an appended null-byte, but this temporary is not bound to any variable name, so when that line ends, Rust drops all the (non-borrowed) temporaries, so it drops it, freeing the heap-allocated contents.

You should do:

let pv: *const c_char = b"SR-DI-DCCT-01:SIGNAL\0".as_ptr().cast();

when dealing with a string literal, or

let cpv: &CStr = &CString::new(pv).unwrap(); // borrows the CString so it is not dropped
// use cpv.as_ptr() at call site:
ca_create_channel(cpv.as_ptr(), ...)

otherwise. Basically you avoid having raw pointers to non-'static locals.


Finally,

    let mut chan_id = std::mem::MaybeUninit::zeroed();
    let rc = unsafe {
        ca_create_channel(
            cpv, on_connect, std::ptr::null(), 0, chan_id.as_mut_ptr()) };
    let chan_id = unsafe { chan_id.assume_init() };

Using MaybeUninit is very dangerous. You are actually using it correctly, (especially because you are using zeroed() rather than uninit()), so kudos for that, but given that chan_id is just a pointer, using easy-to-misuse machinery like MaybeUninit for it seems overkill: you can get a NULL pointer with ::std::ptr::null_mut(), or, equivalently, 0 as _.

2 Likes

Btw, this seems like a very obvious footgun, especially since it (rightly) doesn't require unsafe. It would be interesting to see if the borrow checker could be expanded to also lint on things like this that are not technically invalid Rust code, but almost certainly will be. Of course, because lifetimes are erased from raw pointers, you'd have to work backwards to determine what the implied lifetime of the pointer is.

Wow. This looks like exactly the problem: if the string passed to ca_create_channel has had its memory recycled the most likely effect will be exactly what I'm seeing: nothing! (Won't actually get to try this until Monday...)

I still have a question about this solution (which I'll definitely tick if it works): how long is the newly declared cpv guaranteed to live? If the raw pointer doesn't keep it alive, which I think I understand (I guess raw pointers don't do lifetime), what is keeping it alive? In particular, what ensures it lives long enough to get passed into ca_create_channel?

First of all, since now you are using something with a lifetime that Rust understands, if you get this "estimation of life" wrong, you'll get a compiler error, instead of the program just compiling as it just did for you.
In other words, even if my hypothesis of lifetime of Rust temporaries was wrong and you tried to apply my suggestion, the worst that would happen is your program not compiling, which is great :slightly_smiling_face:

  • Here is a Playground link of me trying to "misuse" this pattern and thus getting a compilation error.

Now, in practice, the rules regarding temporaries that I have inferred / extrapolated, are the following:

  • if the temporary is directly3 borrowed1 into "a new binding through a let declaration"2,

    1. through some mechanism involving actual lifetimes, contrary to raw pointers.

    2. I have experimented through assignments and Rust seems to always then pick a short-lived local / fallback to the "otherwise" case.

    3. Basically the case let it: &() = &*RefCell::new(()).borrow(); does not work, since there are two temporaries involved: the RefCell<()> itself and the Ref<'_, ()> guard that borrows it, and since we are "only" borrowing the latter the former fallbacks to the "otherwise" case, gets dropped, and makes both the Ref temporary and our it unusable.

    then, the temporary is kept alive until the end of the scope containing the let declaration.

    fn f () -> Something
    ;
    
    {
      let borrow: &Something = &f();
      // stuff...
    } // <- the `Something` temporary is dropped here
    // thus `borrow` is only valid up until that point.
    

    that is, it is equivalent to:

    {
      let __temp__ = f(); // anonymous / unnameable
      let borrow = &__temporary__;
      // stuff...
    } // <- `__temp__` is dropped here
    
    
  • otherwise,

    the temporary is dropped right after the declaration / assignment:

    {
      let n: usize =
          String::from_utf8(
              b"Hello, World!".to_vec() // : Vec<u8>
          )
          .unwrap()
          .len()
      ;
      // stuff...
    }
    
    // becomes
    {
      let n: usize = {
          let __t1__: Vec<u8> = b"Hello, World!".to_vec();
          let __t2__: Result<String, _> = String::from_utf8(__t1__);
          let __t3__: String = __t2__.unwrap();
          __t3__.len()
      }; // <- temporaries are all dropped here.
      // stuff...
    }
    
    • Your .as_ptr() call was equivalent to .len() here, given the lack of lifetimes.

  • It does require unsafe, just elsewhere: in Rust forging raw pointers is always safe. That's why it is so important to try and list the // Safety: ... invariants that justify the soundness of an unsafe {} block. In the OP's example, there would be "cpv does not dangle because the CString is still in scope" and then someone reviewing it (event oneself) would be able to then spot the issue.

That being said, it is indeed a very big footgun, one C++-worthy. With FFI, one must be very careful with this question of what-is-the-lifetime-of-each-local, and keep that in mind, although a lint would indeed be great: I think clippy is able to lint against this, but only because it may have hard-coded the list of as_ptr()-like calls. So it won't detect that for a custom / library-defined function.

  • :thinking: I wonder if a #[borrows] annotation (like #[must_use]) on all .as_ptr()-like functions would enable some kind of global lint. That is, the question here is how the lint would distinguish between .len() and .as_ptr(), and such annotation would enable to #[warn(potentially_dangling_ptr)] or something like that.
1 Like

Thank you very much, now I have the following code which works as expected:

let cpv = std::ffi::CString::new(pv).unwrap();
unsafe { ca_create_channel(cpv.as_ptr(), on_connect, ...) };

Now I am relying on cpv (which is a plain old CString) to survive long enough: does rust guarantee that lets are kept alive until the end of their enclosing scope, even if the associated variable isn't apparently used?

It does sound like there could usefully be a lint on .as_ptr(): I guess the main point is that if the raw pointer has a longer lifetime than the argument to as_ptr() then there's a potential problem. Would this be an adequate test: given b = a.as_ptr() check that lifetime of b is no longer than lifetime of a, otherwise generate a lint warning? Of course, once b is passed as a parameter it's out of the compiler's hands.

I'm a bit embarrassed now that the title of my thread is completely wrong! Does rust have any guarantees about invoking C++ constructors on startup? I've evidently read enough to confuse me on this topic :sweat:.

Yes, 100% guaranteed: a value bound to a variable name (a binding) is .drop()ed at the end of the scope where the binding is defined (unless moved to another binding obv.).

{
  let var = ...;
  // stuff... (that may or may not use var)
} // <- var is dropped here

{
  let var = ...;
  {
       let moved = var;
       // stuff...
  } // <- moved (and thus `var` value) is dropped here
  // stuff...
}
  • When in doubt, you can inspect the Medium (level) Intermediate Representation (MIR) of the code. You can, for instance, find such output from the top-left drop-down menu of the Playground.

That is, by the way, how drop(x) works: it's really just { let _moved = x; }.

  • Once a value is moved into a function, that function may or may not drop it, so the caller code must assume the value no longer exists: for instance, both drop(x) and forget(x) invalidate further uses of x (unless x's type is Copy, but then there are no destructors whatsoever), even if the former does run the destructors whereas the latter does not.

Incidentally, this mechanic is also why you may sometimes see things like:

{
  let _guard = ...; // leading underscore for an unused thing
  // stuff...
} // <- run the drop glue for `_guard` here.
Example
struct CallOnDrop<F : FnMut()> {
    f: F,
}
impl<F : FnMut()> Drop for CallOnDrop<F> {
    fn drop (self: &'_ mut CallOnDrop<F>)
    {
        (self.f)();
    }
}
use ::core::cell::Cell; // multiple mutable references through `&Cell`

let x = Cell::new(42);
{
    let _guard = CallOnDrop { f: || x.set(0) };
    assert_eq!(x.get(), 42);
    stuff(&x);
} // <- when this scope is exited, `x` is guaranteed to be zero.
assert_eq!(x.get(), 0);

This is how ::scopeguard works.


Well, first of all it requires distinguishing .as_ptr() functions from .len() functions, since neither carries a lifetime parameter in the return type of their signature, so as far as Rust is concerned, they both do the same: "copy" some information (potentially) out of the borrowed thing.

But I do think that there should be a way to annotate fn as_ptr ()-like functions precisely to express that they are returning some raw handle whose semantics are equivalent to that of a borrow, so as to enable linting against CString::new(...).unwrap().as_ptr(): it is neither the first nor the last time this "bug" is observed in these forums.

Well, you shouldn't worry about it: you had a bug and suspected some cause for it, but it turned out to be due to some other cause :upside_down_face:

That being said, now that the thread has reached this point, the topic could indeed be renamed, or the thread could be split, to make it easier for people with similar CString...as_ptr() issues to stumble this discussion, I guess. I'll let moderators be the judge of it (e.g., cc @mbrubeck)

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.