Pass strings to and from C so library

Hi community,

I wanted to learn, in rust, how to pass strings to c language and receive strings back from c. I simply wrapped strncpy in a new function strncpy2 and build it into so library named libhello.so . My rust code then calls this new strncpy2 for test.

I need to define an array in rust:
let mut dest: [i8; len] = [0; len];

to receive the content in buf pointed by dst in strncpy in c:
char * strncpy(char * dst, const char * src, size_t len);

Is this right way to get strings from c ?

Is it correct for me to convert i8 array to u8 pointer in main.rs: 17, 24?

Can I link the so library and specify its path and name in a configuration file like Cargo.toml instead of build.rs ? I want to link so library way similar to LDFLAGS, LDLIBS.

Thanks.



$ find . -print | sed -e 's;[^/]*/;|____;g;s;____|; |;g'

.
|____Cargo.toml
|____Cargo.lock
|____build.rs
|____src
| |____main.rs
| |____hello
| | |____hello.h
| | |____Makefile
| | |____hello.c
| | |____libhello.so
$

$ cat src/main.rs
extern crate libc;
use libc::{c_char};
use std::{slice, str};

extern "C" {
    fn strncpy2(dest: *mut c_char, src: *const c_char, n: usize) -> *const c_char;
}

fn main() {
    const len: usize = 1024;
    let src = "hello world";
    let mut dest: [i8; len] = [0; len]; //converted to u8 at line 17, 24

    unsafe {

        let s = str::from_utf8_unchecked(
            slice::from_raw_parts(dest.as_ptr() as *const u8, src.len())); //line 17
        println!("{}: |{}|", line!(), s.to_string());

        strncpy2(dest.as_ptr() as *mut c_char,
                 src.as_ptr() as *const c_char, len - 1);

        let s = str::from_utf8_unchecked(
            slice::from_raw_parts(dest.as_ptr() as *const u8, src.len())); //line 24
        println!("{}: |{}|", line!(), s.to_string());

    };

}
$

$ cat build.rs
fn main() {
    // ./src/hello/libhello.so
    let path = "./src/hello";
    let name = "hello";
    println!("cargo:rustc-link-search={}", path);
    println!("cargo:rustc-link-lib={}", name);
}
$

$ cat src/hello/hello.c
#include <string.h>
#include "hello.h"

char *strncpy2(char *dest, const char *src, size_t n)
{
    strncpy(dest, src, n);
}
$

  1. Yes, it is a common pattern to use a [u8; N] or a Vec<u8> as a destination buffer for C strings. However, if the function writes into it, then you cannot use dest.as_ptr(), since the compiler assumes that the data behind it is read-only. Instead, you must use dest.as_mut_ptr().

    Tangentially, you must be careful about how the data is stored. In C, there are two common patterns for strings:

    • a single pointer to a character sequence ending in a null character,
    • or a (pointer, length) pair that refers to a known-length buffer, possibly containing null characters.

    Each representation must be handled in a different way. In particular, one must be careful using the strncpy() function: it will write to the destination buffer up to the first null character in the source string, unless the source string is too long, in which case no null character will be written to the destination buffer.

  2. Yes, this is valid, since both i8 and u8 have the same size, alignment, and validity requirements. Rust does not have an equivalent of C's type-based aliasing rules.

  3. You can specify the name of the library with a link attribute on the extern "C" block. The library name and search directory can alternatively be set using the build.rustflags key in .cargo/config.toml, but this is considered far less idiomatic than using a build script.

3 Likes

Hi,

Thanks for pointing this out, I should call strncpy2 with LEN-1 instead of src.len()

let mut dest: [i8; LEN] = [0; LEN];
strncpy2(dest.as_mut_ptr() as *mut c_char,
         src.as_ptr() as *const c_char, LEN - 1); // not src.len()

My test compiles with build.rs now.

I want to use .cargo/config.toml but there is an error. I do not know what to put in this file and should I change other files too?

Thanks.



$ cat .cargo/config.toml

# link this please:  ./src/hello/libhello.so

[build]
target=["hello"]

#[target.x86_64-unknown-linux-gnu.hello]

[target.hello]
rustc-link-lib = ["hello"]
rustc-link-search = [".src/hello"]
#rustc-flags = "-L ./src/hello"
#rustc-cdylib-link-arg = ["…"]
$

$ LD_LIBRARY_PATH=./src/hello/ cargo run
error: expected a table, but found a array for `target.hello.rustc-link-lib` in /home/ljh/Documents/hello_rust/.cargo/config.toml
$

$ find . -print | sed -e 's;[^/]*/;|____;g;s;____|; |;g'

.
|____Cargo.toml
|____build-rs-renamed  # in favor of .cargo/config.toml
|____src
| |____main.rs
| |____hello
| | |____hello.h
| | |____Makefile
| | |____hello.c
| | |____libhello.so
$

You'd want to use the direct build.rustflags key to set the flags:

[build]
rustflags = ["-Lnative=./src/hello", "-ldylib=hello"]

However, this may cause errors if the current directory is different than what you expect.

1 Like

Hi, thanks a lot.

I have to use absolute path start from root dir / . The relative path does not work for me: "-Lnative=./src/hello", "-Lnative=../src/hello", the .cargo is at package root.

Yeah, it's not very clear what the path is relative to; there have been a few questions on GitHub and Stack Overflow over the years, which seem to have inconsistent conclusions. Usually, with a build script, you'd construct an absolute path programmatically, using env::var("CARGO_MANIFEST_DIR") to get the path to the package root:

use std::env;

fn main() {
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let path = format!("{manifest_dir}/src/hello");
    let name = "hello";
    println!("cargo:rustc-link-search=native={path}");
    println!("cargo:rustc-link-lib=dylib={name}");
}

The build script could also be written to read the settings from a local file, if that is what you are looking for.

1 Like

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.