Search code examples
rustreadlineffi

Readline custom completer


I'm trying to write a readline custom completer (tab completion) in Rust. I think I have everything straight, but when I try it en vivo it heads off into the weeds and never comes back. Oddly, when I call it directly from main() I appear to get a valid pointer back. I never see a crash or panic in either case. Backtrace output is not consistent over runs (it's busy doing something). Perhaps one clue is that gdb indicates that the arguments passed to the completer are incorrect (although I'm not actually using them). Eg, after callback:

#2  0x00007f141f701272 in readlinetest::complete (text=0x7f141ff27d10 "", start=2704437, end=499122176) at src/main.rs:24

Or directly, breakpointing the call in main:

#0  readlinetest::complete (text=0x555555559190 <complete::hcda8d6cb2ef52a1bKaa> "dH;$%p", start=0, end=0) at src/main.rs:21

Do I have an ABI problem? Seems unlikely and the function signature isn't complicated :(

Here is a small test project: Cargo.toml:

[package]
name = "readlinetest"           
version = "0.1.0"
authors = ["You <[email protected]>"]

[dependencies]
libc = "*"
readline = "*" 

And main.rs:

extern crate libc;
extern crate readline;

use libc::{c_char, c_int};
use std::ffi::CString;
use std::process::exit;
use std::ptr;
use std::str;

extern { fn puts(s: *const libc::c_char); } 

#[link(name = "readline")]
// Define the global in libreadline that will point to our completion function.
extern {
    static mut rl_completion_entry_function: extern fn(text: *const c_char,
                                                       start: c_int,
                                                       end: c_int) -> *const *const c_char; 
} 

// Our completion function. Returns two strings.
extern fn complete(text: *const c_char, start: c_int, end: c_int) -> *const *const c_char {
    let _ = text; let _ = start; let _ = end;
    let mut words:Vec<*const c_char> =
        vec!(CString::new("one").unwrap(), CString::new("two").unwrap()).
        iter().
        map(|arg| arg.as_ptr()).
        collect();
    words.push(ptr::null()); // append null
    words.as_ptr() as *const *const c_char
} 

fn main() {
    let words = complete(string_to_mut_c_char("hi"), 1, 2);
    unsafe { puts(*words) } // prints "one"
    //unsafe { puts((*words + ?)) } // not sure hot to get to "two"
    unsafe { rl_completion_entry_function = complete }
    // Loop until EOF: echo input to stdout
    loop {
        if let Ok(input) = readline::readline_bare(&CString::new("> ").unwrap()) {
            let text = str::from_utf8(&input.to_bytes()).unwrap();
            println!("{}", text);
        } else { // EOF/^D
            exit(0)
        }
    }
}

// Just for testing
fn string_to_mut_c_char(s: &str) -> *mut c_char {
    let mut bytes = s.to_string().into_bytes(); // Vec<u8>
    bytes.push(0); // terminating null
    let mut cchars = bytes.iter().map(|b| *b as c_char).collect::<Vec<c_char>>();
    let name: *mut c_char = cchars.as_mut_ptr();
    name

}

Ubuntu 14.04, 64 bit with Rust 1.3.

What am I missing? Thanks for any pointers (ha ha...).


Solution

  • and the function signature isn't complicated

    It's not, but it does help to have the right one... ^_^ From my local version of readline (6.3.8):

    extern rl_compentry_func_t *rl_completion_entry_function;
    typedef char *rl_compentry_func_t PARAMS((const char *, int));
    

    Additionally, you have multiple use after free errors:

    vec![CString::new("one").unwrap()].iter().map(|s| s.as_ptr());
    

    This creates a CString and gets the pointer to it. When the statement is done, nothing owns the vector that owns the strings. The vector will be immediately dropped, dropping the strings, invalidating the pointers.

    words.as_ptr() as *const *const c_char
    

    Similar thing here — you take the pointer, but then nothing owns the words vector anymore, so it is dropped, invalidating that pointer. So now you have an invalid pointer which attempts to point to a sequence of invalid pointers.

    The same problem can be found in string_to_mut_c_char.

    I don't know enough readline to understand who is supposed to own the returned strings, but it looks like you pass ownership to readline and it frees them. If so, that means you are going to have to use the same allocator that readline does so that it can free the strings for you. You will likely have to write some custom code that copies a CString's data using the appropriate allocator.


    Style-wise, you can use underscores in variable names to indicate they are unused:

    extern fn complete(_text: *const c_char, _start: c_int, _end: c_int)
    

    There should be a space after : and there's no need to specify the type of the vector's contents:

    let mut words: Vec<_>