Newbie FFI: Segfaults and undefined symbols for architecture

I've come back to working on FFI bindings for TaskWarrior (a C++ project), which I had some some incredible help on last year (thanks again!):

I had a few things working by creating a wrapper.h and wrapper.cpp thanks to @MoAlyousef and an analogous CXX example contributed by @dtolnay.

I wanted to revisit to see if I could get things working just by using the TaskWarrior header files and shared libraries directly -- which may not be possible, I suppose.

Currently, I'm focusing on the Context class, which seems to do most of the heavy lifting. I can get it to build with this build.rs:

The resulting binary in ../target/debug/build/... runs and the layout tests pass.

However, it doesn't work with a simple smoke test. Looking at the generated bindings, I created two different versions of a very basic test, modeled after [taskwarrior's main.cpp code][1].

The code in question is in test_new, here.

It constructs a fake CLI argument list and just runs list; I had this part working before, so I think it's code is ok.

I was hoping to use the rusty-looking bindings, which I got to compile (but not link) like so:

let mut ctx = Context::new(Context_context);
Context::setContext(&mut ctx);
let status = ctx.initialize(i32::try_from(argv.len() - 1)?, argv.as_mut_ptr());
dbg!(status);
// let status = ctx.run();
// dbg!(status);

Unfortunately, this gives me what I think is a linking error at test time, right at the first line (Context::new(Context_context);):

  = note: Undefined symbols for architecture arm64:
            "Context::Context(Context const&)", referenced from:
                taskwarrior_sys::Context::new::hbd1a16f83b64984f in taskwarrior_sys-e91a44beb3be72f4.r25ysju3oi22kyb.rcgu.o
          ld: symbol(s) not found for architecture arm64
          clang: error: linker command failed with exit code 1 (use -v to see invocation)

The generated bindings include:

extern "C" {
    #[link_name = "\u{1}__ZN7ContextC1ERKS_"]
    pub fn Context_Context(this: *mut Context, arg1: *const Context);
}
...
impl Context {
    #[inline]
    pub unsafe fn new(arg1: *const Context) -> Self {
        let mut __bindgen_tmp = ::std::mem::MaybeUninit::uninit();
        Context_Context(__bindgen_tmp.as_mut_ptr(), arg1);
        __bindgen_tmp.assume_init()
    }
}

I've tried changing my build.rs a hundred different ways to allowlist_{type,function,var} .*[cC]ontext.*, but it doesn't seem to change anything.

I can see that __ZN7ContextC1ERKS_ exists in a few places:

$ rg -uuuFl '__ZN7ContextC1ERKS_' ../target/
../target/debug/deps/libtaskwarrior_sys-ee3ffa94e667fce4.rlib
../target/debug/deps/libtaskwarrior_sys-ee3ffa94e667fce4.rmeta
../target/debug/deps/taskwarrior_sys-e91a44beb3be72f4.r25ysju3oi22kyb.rcgu.o
../target/debug/build/taskwarrior-sys-c885456ffaf97d6f/out/bindings.rs
../target/debug/incremental/taskwarrior_sys-2vr0tyl3mgg5z/s-gbl50gj9tl-mpb122-1oquhnrfll5mc/r25ysju3oi22kyb.o
../target/debug/incremental/taskwarrior_sys-3tmt2vpuboc4y/s-gbl50gj9tm-wqcw88-2vqm4g5d9uo/query-cache.bin
../target/debug/incremental/taskwarrior_sys-2vr0tyl3mgg5z/s-gbl50gj9tl-mpb122-1oquhnrfll5mc/query-cache.bin
../target/debug/libtaskwarrior_sys.rlib

Instead of the rusty-looking bindings, if I use the extern "C" bindings (with the underscore) directly, I get a little further:

let ctx = Context_context;
  Context_setContext(ctx);
  let status = Context_initialize(
    ctx,
    i32::try_from(argv.len() - 1)?,
    argv.as_mut_ptr(),
);
dbg!(status);
// let status = Context_run(Context_getContext());
// dbg!(status);

In this case, I get a segfault at Context_initialize.

Questions:

  1. In the first case, what symbol is undefined, and why?
    • I assume Context::Context, but could it be Context (just before the const)?
    • I think I'm linking all the shared libraries and allowlisting all related types, and similar code links from the "underscore" approach -- why is it undefined?
  2. In the second case, I think I'm following pretty closely what the original cpp code does (below) -- why does it segfault from Rust?
int status {0};
Context globalContext;
Context::setContext (&globalContext);
status = Context::getContext ().initialize (argc, argv);
if (status == 0)
    status = Context::getContext ().run ();

Many thanks in advance for any pointers and suggestions!


  1. taskwarrior/main.cpp at develop · GothenburgBitFactory/taskwarrior · GitHub ↩︎

What is the definition and type of argv?

let cmd = CString::new("list")?;
let mut argv: Vec<*const c_char> =
            vec![CString::new("")?.as_ptr(), cmd.as_ptr(), std::ptr::null()];

To match up with:

int main (int argc, const char** argv)
...
status = Context::getContext ().initialize (argc, argv);

I haven't made any changes to argv since I had it working with the minimal C wrapper approach, which is still available on the master branch: https://github.com/n8henrie/taskwarrior-rs/tree/master/taskwarrior-sys

CString::new("")?.as_ptr()

Allocates, gets a ptr, then at the end of the statement frees the string. So you have a use after free bug.

You should change it to CStr::from_bytes_with_nul instead

CStr::from_bytes_with_nul(b"\0")?.as_ptr()
1 Like

Thanks for pointing that out!

Doesn't seem to change the behavior of the issues above, but I'm glad to know.

I believe this is the mission statement of the google/autocxx repository - have you investigated that yet?

Yes, I did about a year ago and couldn't get it to work.

Just tried again today:

fn initialize(_uhoh: BindingGenerationFailure)
autocxx bindings couldn’t be generated: Pointer pointed to something unsupported

¯\_(ツ)_/¯

1 Like