#Linking (or dynamically calling into) to a .so library that doesn't exist on my build machine

1 messages · Page 1 of 1 (latest)

ebon ember
#

I'm writing a program for raspberry pi but compiling it on my mac to speed up the compilation. I've got the basic cross-compilation working but now struggling to figure out how to use pigpio, a GPIO c shared library already on the raspberry pi.

I've tried these approaches (all separate approaches)

in build.zig, attempting to at least point the build at the link location:

  • exe.addSystemIncludePath -> build expects a local version of pigpio
  • exe.linkSystemLibrary2("pigpio", .{.preferred_link_mode = .dynamic}); -> also expects local version

in main.zig, attempting to do the "import" and library calls at runtime

  • std.DynLib.open("/lib/libpigpio.so") -> some ElfHashTableNotFound error at runtime

in main.zig, attempting to do normal @cImports -> expects a local version of pigpio

in pigpio.zig, attempting to just declare extern functions and hoping calling them resulted in the symbols being found on the host machine's lib

pub extern fn gpioInitialise() c_int;
pub extern fn gpioSetMode(gpio: c_uint, mode: c_uint) c_int;
pub extern fn gpioWrite(gpio: c_uint, level: c_uint) c_int;
pub extern fn gpioTerminate() void;

Excuse me if any of this is naive, I'm not experienced with building C. I've done some FFI stuff from dynamic langauages into .so libraries but never compiled languages.

Is there a simple solution here?

swift ruin
#

For cImport, you need to have a local version of pigpio header files and they have to be added with exe.addIncludePath(). If you can't have headers on the development machine then you have to declare extern functions manually so zig knows the function signatures

ebon ember
#

Thanks for clarifying! And then at runtime how do the extern functions know to call into the .so? Is it like linux PATH where the path is searched through to resolve it?

#

Better worded, is there anything I need to do in build or on the target machine to specify that /lib or /lib/libpigpio.so are the place to look for the implementation of these extern fn symbols?

#

Such as export LD_LIBRARY_PATH=/path/to/pigpio:$LD_LIBRARY_PATH

swift ruin
#

I don't know how that part works unfortunately. I would assume you have to set a flag on compilation to say you want to link with a .so file in a certain place when loading the binary but I don't know what the flag is

steep walrus
#

My general advice is to not rely on the so-called RPATH (runtime path; the place the dynamic linker expects to find the SO at runtime), and instead just put the SO next to the exe, which I believe is implicitly checked(?)

ebon ember
#

ok thanks! will give it a try

ebon ember
#

Current status: not yet working

This seems to be the closest reference to my issue
https://github.com/ziglang/zig/issues/8180

build.zig:

const std = @import("std");

// zig build --summary all

pub fn build(b: *std.Build) void {
    const target = b.resolveTargetQuery(.{
        .abi = .gnueabihf,
        .cpu_arch = .arm,
        .os_tag = .linux,
        .cpu_model = std.Target.Query.CpuModel{ .explicit = &std.Target.arm.cpu.arm1176jz_s },
    });
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "zigblink",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    // I'd hoped this would be the secret sauce, but no 
    exe.linker_allow_shlib_undefined = true;
    exe.linkLibC();

    b.installArtifact(exe);
}

main.zig

const std = @import("std");
const pigpio = @import("./pigpio.zig");

pub fn main() !void {
    _ = pigpio.gpioInitialise();
    defer pigpio.gpioTerminate();

    std.debug.print("Hello, world from rpi!\n", .{});
}

pigpio.zig

pub extern "pigpio" fn gpioInitialise() callconv(.C) c_int;
pub extern "pigpio" fn gpioSetMode(gpio: c_uint, mode: c_uint) callconv(.C) c_int;
pub extern "pigpio" fn gpioWrite(gpio: c_uint, level: c_uint) callconv(.C) c_int;
pub extern "pigpio" fn gpioTerminate() callconv(.C) void;
#

Compiling this on my machine yields this error

zig build --summary all
install
└─ install zigblink
   └─ zig build-exe zigblink Debug arm-linux-gnueabihf 2 errors
error: ld.lld: undefined symbol: gpioInitialise
    note: referenced by main.zig:5
    note:               /Users/erik/pig/zigport/.zig-cache/o/bbd07a5ad4b1cb3d82de1b7e44e14cd7/zigblink.o:(main.main)
error: ld.lld: undefined symbol: gpioTerminate
    note: referenced by main.zig:6
    note:               /Users/erik/pig/zigport/.zig-cache/o/bbd07a5ad4b1cb3d82de1b7e44e14cd7/zigblink.o:(main.main)
error: the following command failed with 2 compilation errors:
/opt/homebrew/Cellar/zig/0.13.0/bin/zig build-exe -ODebug -target arm-linux-gnueabihf -mcpu arm1176jz_s -Mroot=/Users/erik/pig/zigport/src/main.zig -lc -fallow-shlib-undefined --cache-dir /Users/erik/pig/zigport/.zig-cache --global-cache-dir /Users/erik/.cache/zig --name zigblink --listen=-

which is strange since the -fallow-shlib-undefined flag is in there

#

on Zig version 0.13.0 btw

ebon ember
#

ok I've abandoned above attempt because it feels like it should work according to the linked github issue, and perhaps is a bug that it doesn't work for me. bummer, would have been "elegant" to have extern fn definitions just work.

I've reverted to using DynLib manually, and managed to get it working. Working code here:
https://gist.github.com/erik-dunteman/ee4b68e5aa97b92bc0bea95aa173e123

If anyone can give tips on what could possibly be wrong with my extern fn approach, I'd love to move back to it. But will roll with DynLib for now.

Gist

GitHub Gist: instantly share code, notes, and snippets.

swift ruin
#

I think that the linker needs to know which library the symbols come from maybe? You can make a stub library that defines the needed functions and symbols and link against that (command line zig build-lib stub.zig -dynamic) and make sure the path is right so it can find the library on the real machine

steep walrus
#

You have to have the library that contains those functions on the current machine, compiled or otherwise, and link with them via addObjectFile or the like.

#

Or, have the (presumably) C source code and compile that using Zig via addCSourceFiles.

#

But either way, the linker must be able to find those symbols somewhere if you're going to use extern fn, at build-time.

#

DynLib gets around that because you then only load the symbols at runtime; the exe itself has no idea about any of that, so you're good.

#

But extern fn puts that info into the exe file, so it needs to be around at compilation time - otherwise it cannot link with it.

low zodiac
#

I think the extern fns would work with -fallow-shlib-undefined maybe?

#

At least that's how I build code that uses lua.h to tell the linker to ignore anything undefined and assume it will be available at runtime

#

not sure if this is the same sort of thing

swift ruin
#

allow-shlib-undefined is only allowed when building a shared library

#

maybe for an executable, the linker needs to know which .so file the symbol comes from, but not for a shared library? I'm not sure why it's not allowed for an executable

ebon ember
# swift ruin allow-shlib-undefined is only allowed when building a shared library

confirming this is the case. allow_shlib_undefined only works when building shared libraries. Not in executable builds.

For example this builds:

build.zig

const zigpio = b.addSharedLibrary(.{
        .name = "zigpio",
        .root_source_file = b.path("libs/zigpio/src/zigpio.zig"),
        .target = target,
        .optimize = optimize,
    });

// allow zigpio to link to undefined external symbols
zigpio.linker_allow_shlib_undefined = true;

and I suspect I can then build that resulting .so into my executable. Annoying thing there is that I now have two layers of "extern" functions, since executable calls into zig-based .so file, and then that zig-based .so file calls into the c library on the target machine.

UPDATE: doing the two-layer approach fails. When I build a zig lib as .so that wraps the not-present libpigpio.so file, it successfully builds the lib. But then when I extern call into that lib from an executable, that build fails. It seems that the .exe build basically drills through multiple layers of libs, and doesn't allow any symbols to be missing.

swift ruin
#

I guess theoretically you could put your whole app in the .so file in that case and have main() of the executable call main in the so file?

#

Seems weird

ebon ember
#

such an insane idea that it actually may just work lol

#

i'm just going to stick to DynLib for this

#

if I did want to do the double-extern thing I could make a "common.zig" file with all the extern definitions so at least they're only in one place, but that feels like such a hack

#

actually will try it for fun, maybe it's not so bad

ebon ember
#

final update:

the two-layer approach fails. I can successfully build a zig lib as .so without having the libpigpio.so on my build machine, using the allow_shlib_undefined=True flag. But then when I try to build that .so into an executable, the exe build doesn't allow undefined libs, even if they're multiple layers deep.

As far as I can tell, all lib symbols must be present on build machine if you're building an executable.