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.)

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.