FFI bindings for taskwarrior

Hi all,

I'm a novice continuing to learn about FFI, with no C/C++ background.

After my last thread, I thought that perhaps taskwarrior would be a cool project to build FFI bindings for, and work on a safe rust wrapper*. With cmake -DCMAKE_BUILD_TYPE=release && make it builds without issue on both my M1 Mac and Linux x86_64 machines.

I think it looks like a better candidate for FFI than my prior project as it has most of the functionality exposed in shared libraries; here is the main.cpp, see several in the compiled C++ project:

$ fd -u '^lib.*\.a$'
src/columns/libcolumns.a
src/commands/libcommands.a
src/liblibshared.a
src/libtask.a

Unfortunately, with either of my machines I can't get it to build and link with cmake-rs. After lots of tinkering, here is the most recent iteration of my build.rs (and my only test currently just calls let context = Context_getContext();, which I think should be the proper name-mangled function after looking at bindings.rs).

use std::env;
use std::path::PathBuf;

use cmake::Config;

fn main() {
    println!("cargo:rerun-if-changed=wrapper.h");

    let dst = Config::new("vendor/taskwarrior").profile("release").build();

    println!(
        "cargo:rustc-link-search=native={}/build/src/columns",
        dst.display()
    );
    println!(
        "cargo:rustc-link-search=native={}/build/src/commands",
        dst.display()
    );
    println!("cargo:rustc-link-search=native={}/build/src", dst.display());
    println!("cargo:rustc-link-lib=static=task");
    println!("cargo:rustc-link-lib=static=libshared");
    println!("cargo:rustc-link-lib=static=columns");
    println!("cargo:rustc-link-lib=static=commands");

    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
        .clang_arg("-xc++")
        .clang_arg("-lc++")
        .clang_arg("-std=c++17")
        .clang_arg("-stdlib=libc++")
        .clang_arg("-Ivendor/taskwarrior/src")
        .clang_arg("-Ivendor/taskwarrior/src/commands")
        .clang_arg("-Ivendor/taskwarrior/src/columns")
        .clang_arg("-Ivendor/taskwarrior/src/libshared/src")
        // .allowlist_type("Context")
        .allowlist_function("Context_getContext")
        .generate()
        .expect("Unable to generate bindings");

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}
// wrapper.h
#include "Context.h"
static Context& getContext ();

I keep getting linking errors about not being able to find types that I think should be available in the C (EDIT: C++ rather) stdlib:

 = note: Undefined symbols for architecture arm64:
            "std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::~basic_string()", referenced from:

or being unable to find operator new(unsigned long) or operator delete(void*). All of my SO searches and googling make it seem that pointing clang towards the correct stdlib with .clang_arg("-stdlib=libc++") and .clang_arg("-lc++") should be the solution, but they don't seem to help.

I think I'm probably just failing to link something needed by C++, but having looked through all of the cmake output in the compiled C++ project, I can't find any paths that I'm missing.

I've seen some of the bindgen disclaimers about C++ and realize this might not be a smooth road, but after failing to make progress on this several days in a row I was wondering if anyone sees any obvious step that I'm missing.

TIA for any pointers, as always!

* Also wanted to point out https://github.com/taskchampion/taskchampion which looks like a really cool TW-inspired project in Rust, and even notes that it 'currently functions as a "testbed" for new functionality that may later be incorporated into TaskWarrior,' though it looks like the intersection of committers in the two projects is @dustin alone.

C and C++ are distinct and different languages. If you want to use C++, you need to link against the C++ standard library. This is most commonly done by invoking g++ or clang++ instead of gcc or clang at the linking stage. What are you using for linking?

Right, I'm aware of at least that much. I only see one place that I mistakenly referred to C instead of C++ -- are there places that I'm implying or referring to C that I'm not aware of? taskwarrior is a C++ project.

I thought that's what this was for:

But maybe I need additional linking?

I was following the pattern at https://docs.rs/cmake/0.1.46/cmake/#examples but it looks like there are a few issues suggesting there's more to the story:

1 Like

What OS are you on? On macOS, the default invocation of clang++ doesn't work. For some reason, Apple thought it would be a good idea if absolutely everything had to go through Xcode. Therefore, on macOS, you have to call the compiler like xcrun -sdk macosx clang++, otherwise it won't know about the path of the standard library (which is located under Xcode's app bundle, and not in /usr/lib like every normal OS).

1 Like

Huh. I assumed since cmake figured it all out that cmake-rs would as well, but clearly that's a big assumption on my part.

I've been trying to get this working on both an M1 Mac on Monterey and an x86 Arch Linux machine. The error messages copied above were from MacOS, but the x86 Linux errors were similar if memory serves (I will verify later).

Also, I believe I have clang installed with LLVM on MacOS -- I wonder if it would be better or worse to specify that clang++ (and include path).

These clang_args are for bindgen which only finds the necessary headers to find C/C++ symbols. You’ll need to link to the C++ stdlib in

println!("cargo:rustc-link-lib=c++"); on MacOS.

println!("cargo:rustc-link-lib=stdc++"); on Linux.

3 Likes

That was it! Thanks a ton, I should have asked a week ago. I'll push a preliminary commit and post a link soon.

(Obviously,) I still get confused about linking -- what is done at compile time vs run-time, where to pass flags to find header files vs links to shared libraries, etc. Lots to learn!

I was thinking the same – it just didn't seem right that bindgen was somehow supposed to do the linking. I don't use bindgen-as-a-crate, though (I prefer writing portable C and generating headers only once), so I wasn't sure if it had this capability.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.