Error when compiling/linking with .o files

I have something working in C, and I'm getting an error trying to do the same in Rust. If anyone can help, I'd really appreciate it!

Background

I need to build an executable by linking two .o files which are built separately on different machines. Here's a minimal example in C:

app.c

char* run() {
  return "hello!";
}

host.c

#include <stdio.h>

extern char* run();

int main() {
  printf("app says: %s\n", run());

  return 0;
}

Here's how I build and run the combined application:

$ clang -c host.c -o host.o
$ clang -c app.c -o app.o
$ ld -lc host.o app.o -o app
$ ./app
app says: hello!

Importantly, I have to build the executable by building host.c and app.c separately (because in the actual use case this would happen on separate machines). Once I have host.o and app.o, I can link them together with ld to get the app executable.

Rust Version

I'm trying to repeat this process with a host.rs instead of host.c, leaving everything else the same.

Here's the first host.rs implementation I tried:

host.rs

use std::ffi::CStr;
use std::os::raw::c_char;

extern "C" {
    fn run() -> *const c_char;
}

pub fn main() {
    let c_str = unsafe { CStr::from_ptr(run()) };

    println!("app says: \"{}\"", c_str.to_str().unwrap());
}

I tried to build this, and got:

$ rustc host.rs -o host.o
error: linking with `cc` failed: exit code: 1
  |
  = note: "cc" [ …lots of cc args snipped… ]
  = note: Undefined symbols for architecture x86_64:
            "_run", referenced from:
                host::main::h9e67e7b31c940a95 in host.host.7rcbfp3g-cgu.4.rcgu.o
          ld: symbol(s) not found for architecture x86_64

Next I tried adding #![crate_type = "staticlib"] to the top of host.rs. That successfully generated host.o, but then the next step in the process failed - the ld call:

$ ld -lc host.o app.o -o app
Undefined symbols for architecture x86_64:
  "_main", referenced from:
     implicit entry/start for main executable
ld: symbol(s) not found for architecture x86_64

I tried messing around with a few things, including trying #![crate_type = "lib"] instead of staticlib, and also changing the function's name from main to start. I even tried this trick to avoid LLVM name mangling:

#[export_name = "\x01start"]
pub extern "C" fn start(_argc: isize, _argv: *const *const u8) -> isize {

However, even after having removed all instances of the string main from all source files, and recompiling, ld is still complaining about not finding the symbol _main. (This is on macOS 10.15.6, if that's relevant.)

I'm not sure what I should be telling rustc (or maybe I should try getting cargo involved?) to get it to emit the same .o file that the C version is doing, but I assume there's some combination of arguments that will do the trick!

Any help would be greatly appreciated. :heart:

Aside:

- char * run ()
+ char const * run ()
  {
      return "hello!";
  }

Now, back to the topic at hand, the way to interact with Rust linking steps is at the library level (e.g., .a), not the object file (.o) level. Although I can imagine it being possible with some arcane instanciation to hook into the actual linker invocation and provide your own .o files, it just isn't really idiomatic / the way to be done.

Luckily, there is a trivial way to convert a .o file into a library, since archiving / bundling several .o files into a .a archive using the ar tool is precisely the way to create a static library. In our case, instead of "several", we will have just one:

  1. # Create the `.a` static library
    clang -c host.c && ar -rcs libhost.a host.o
    
    • For context, I'm gonna translate to Rust the following compilation command:

      #                    lib    .a (or .so)
      cc -o app app.c -L . -l host
      #                  ^
      # more generally, path/to/dir/containing/libhost.a
      
  2. //! `build.rs` script to emit `-l host` and `-L path/to/dir/containing/libhost.a`
    
    fn main ()
    {
        // -L
        println!("cargo:rustc-link-search=native={}",
            "path/to/dir/containing",
        );
        // _e.g._, for `-L .`:
        println!("cargo:rustc-link-search=native=.");
    
        // -l host
        println!("cargo:rustc-link-lib=static={}",
            /* lib */ "host" /* .a */,
        );
    }
    
  3. And now, you can use the prototype / extern declaration within Rust;

    use ::std::os::raw::c_char;
    
    extern "C" {
        fn run () -> *const c_char;
    }
    
  4. And even yield a safe wrapper around it. In this case, it yields a 'static C string:

    use ::std::ffi::CStr;
    
    fn run () -> &'static CStr
    {
        extern "C" {
            fn run () -> *const ::std::os::raw::c_char;
        }
    
        unsafe {
            CStr::from_ptr(run())
        }
    }
    
1 Like

Thanks for the reply!

Unfortunately, this is part of my requirement. :sweat_smile:

I probably should have been more explicit about this, but it's unavoidable that the three steps to the process here are:

  1. host.o gets compiled on machine A somehow
  2. app.o gets compiled on machine B somehow
  3. ld gets run on machine B to combine host.o and app.o into an executable

I'm working on machine A in this situation; I can control how host.o gets built and that's it.

So I can't, for example, change the procedure to have rustc or cargo do the final executable linking - that part doesn't happen on my machine, and is totally out of my control!

All I can control is how that host.o is produced, and I'm hoping it's somehow possible for me to do that with Rust instead of C! :smile:

1 Like

Oh, I see. The following thread tried to tackle the issue, but ended up going the .a route:

On my own end I have tinkered witht he following setup, but still have some unwind runtime being expected (_Unwind_Resume):

//! `src/lib.rs`

use ::std::{
    ffi::CStr,
    os::raw::{c_char, c_int},
};

fn run () -> &'static CStr
{
    extern "C" {
        fn run () -> *const c_char;
    }

    unsafe {
        CStr::from_ptr(run())
    }
}

#[no_mangle]
fn _start ()
{
    println!("{}", run().to_string_lossy());
}

followed by:

rustc --crate-type=staticlib --edition=2018 -C panic=abort src/lib.rs -o libfoo.a
# or cargo build [--release]
# but with panic = "abort" as told in the thread, and crate-type = ["staticlib"]

# Explode the `.a` into its many `.o` constituents
rm -rf objs && mkdir objs && (cd objs && ar x ../libfoo.a)
# Merge all of them into a single `.o` ?
ld -relocate objs/*.o foo.o

# Final linking step
ld -o foo foo.o run.o -lc -lpthread -ldl  # Error, _Unwind_Resume
  • Where run.o comes from

    echo 'char const * run () { return "hi"; }' | cc -xc - -c -o run.o
    

I have then also tried to go the #![no_std] road:

#![no_std] #[panic_handler]fn panic(_:&::core::panic::PanicInfo)->!{loop{}}

#[no_mangle]
fn _start ()
{ unsafe {
    extern "C" {
        fn write (_: i32, _: *const u8, _: usize) -> isize;
        fn exit (_: i32) -> !;

        fn run () -> *const u8;
    }

    let s = run();
    let len = 2; // FIXME

    write(1, s, len);
    exit(42);
}}

At which point the steps above can be applied and linking does work. But I cannot run the binary :sweat_smile:

But maybe it's enough for you to tinker with your setup and, hopefully, be more successful than I :slightly_smiling_face:

1 Like

I finally found a solution - it's inelegant, but gets the job done!

First, I made a C file like so:

extern int rust_main();

int main() {
  return rust_main();
}

...then I write my rust_main in Rust with #[no_mangle], compile it into the C file, and compile that into the .o file I need.

Like I said - inelegant, but it works. :smile:

Thanks again for the help @Yandros!

3 Likes

Thank you for sharing this inelegant unconventional solution! I'm pretty sure other people out there stumbling upon the same issue will appreciate your clever workaround :wink:

This should instead be:

rustc host.rs --emit obj

(However, you would still need to find a way to link in the Rust standard libraries, so this still might not be useable for you.)