How to set @cImport file path?

During working with @cImport,
:slightly_smiling_face: when using absolute full path, like:

const c = @cImport(@cInclude("FULL_PATH/demo.h"))

All is good.

:pensive: when using file name, like:

const c = @cImport(@cInclude("demo.h"))

zig build will throw out the error C import failed.
However, VSCode/zls is OK, no file_not_found error prompted and clicking into c symbol definition worked.

I suppose you need to use addIncludePath() on the exe/lib object for a relative demo.h to be found.

@jmc Thanks!
But addIncludePath() is useless. :pensive:

I’m certain it’s not useless, perhaps it wasn’t of help here though :sweat_smile:

Can you share some code then?

OK.

Say, the project structure is like:

proj/
    - build.zig
    - src/
        - main.zig
        - demo.zig
        - croot/
            - cpath/
                - demo.h

In build.zig:

const lib = b.addStaticLibrary("demo", "src/main.zig");
lib.addIncludePath("src/croot/cpath/");

In main.zig:

pub usingnamespace @import("demo.zig");

In demo.zig:

const c = @cImport(@cInclude("demo.h"));
...
...c.xxx...

Then,

PATH_TO_demo.zig:1:11 error: C import failed

Works fine here – with minor changes since I have Zig 0.9.1 on this machine:

$ cat build.zig 
const std = @import("std");

pub fn build(b: *std.build.Builder) void {
    const target = b.standardTargetOptions(.{});
    const mode = b.standardReleaseOptions();

    const lib = b.addStaticLibrary("demo", "src/main.zig");
    lib.addIncludeDir("src/croot/cpath/");
    lib.setTarget(target);
    lib.setBuildMode(mode);
    lib.install();
}

$ cat src/croot/cpath/demo.h 
#define IM_C_CODE_HELLO "yoooooooooooo"

$ cat src/main.zig 
const std = @import("std");
const c = @cImport(@cInclude("demo.h"));

pub fn main() anyerror!void {
    std.log.info("{s}", .{c.IM_C_CODE_HELLO});
}

$ zig build
$ zig version
0.9.1

It’s OK if just building the library.
You can have a try to build a caller executable besides.
Just like:

const exe = b.addExecutable("caller", "caller.zig");
exe.setBuildMode(mode);
exe.addPackagePath("demo", "src/main.zig");
exe.addObjectFile("./zig-out/lib/libdemo.a");
exe.install();

and caller.zig:

const d = @import("demo");

pub fn main() void {
    d.demo();
}

Even so, it should work just fine. Revised example:

$ cat build.zig 
const std = @import("std");

pub fn build(b: *std.build.Builder) void {
    const target = b.standardTargetOptions(.{});
    const mode = b.standardReleaseOptions();

    const lib = b.addStaticLibrary("cinterface", "src/cinterface.zig");
    lib.addIncludeDir("src/croot/cpath/");
    lib.setTarget(target);
    lib.setBuildMode(mode);
    lib.install();

    const exe = b.addExecutable("demo", "src/main.zig");
    exe.linkLibrary(lib);
    exe.setTarget(target);
    exe.setBuildMode(mode);
    exe.install();

    const run_cmd = exe.run();
    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

$ cat src/croot/cpath/demo.h
#define IM_C_CODE_HELLO "yoooooooooooo"

$ cat src/cinterface.zig 
const c = @cImport(@cInclude("demo.h"));

export fn getMessage() [*c]const u8 {
    return c.IM_C_CODE_HELLO;
}

$ cat src/main.zig 
const std = @import("std");

// defined in cinterface.zig, linked statically
extern fn getMessage() [*c]const u8;

pub fn main() anyerror!void {
    std.log.info("{s}", .{getMessage()});
}

$ zig build run
info: yoooooooooooo
2 Likes

Ah, you use linkLibrary() and export/extern.

I want to simulate how to use the published third-party library.
Say, just libdemo.a is accessed, without source code.
Besides, in the caller, I can use @import("demo") to invoke the library interface.

Is it viable?
How can I simulate that scenario?

If you use @import() then that source file/package is added to the sources for the current build. You need access to the sources themselves, and if they import C library headers then your client build also need to be able to see those C library headers. They become all one big compilation unit.

When you try to @cImport() from the client code, the lib.addIncludePath(...) call has no effect on the exe target. If you want to import things that way, you’ll need exe.addIncludePath(...) there too.

No, I just want to hide c code inside library.

So, when we @import("std"), the compiler must find out the zig lib/std/std.zig source file?
I tried to rename the lib/std/std.zig and rebuilt the demo project containing @import("std"), no error prompted.
Is that feasible evidence?

From what I have read I think you want to use a static library that may be compiled separately / by someone else, is that the case?

I think you still at least need a header file that contains declarations of the functions you want to use. You would use that header file with @cImport() to make zig aware of the available functions.

Then you could try adding your libdemo.a via exe.addObjectFile("path/to/libdemo.a"); like you wrote.

However I also think that your libdemo.a needs to be built so that all functions that you want to use from zig are declared with extern.

In your executable you would have:

const demo = @import("demo.zig");

demo.zig:

pub usingnamespace @cImport({
    // should be accessible from a path added via exe.addIncludePath
    // or use absolute path
    @cInclude("demo.h"); 
});

I haven’t tried something like this with zig yet, so there could be things I am missing here.


Take a look at @import.
"std" is special and always available (probably (I guess) It gets compiled into the zig executable, which would explain why you renaming files doesn’t have any effect).

If you want to include something other than those special things provided by zig, you have to use a path to an actual file, which is why your imports also need to mention the file extension .zig not just demo.

(Didn’t see that you had a package path defined, but I am still confused, do you want to build a static library, or use one or both?

I think maybe sepparating it into two experiments one about building a library, one about using one may make things easier.

Also consider publishing a repo with your test setup, that way it would be easier to see all the details of what your test does and maybe see some detail that isn’t right. I am too lazy to try to reverse engineer your setup 1:1 from your comments…)

As far as I can tell builder.addStaticLibrary is for building a static library not for adding the static library to your exe build. And in theory exe.addObject should work to get the implementation part of your built static library into your exe build step, but you still need to add the interface info via the header file.

Hope that helps, so far I haven’t done a lot of experiments in that area of using zig, so I am not 100% sure.

2 Likes

Thanks, and sorry to response late.
I’m studying and trying.
Maybe reply later.

@jmc 's previous code sample shows one viable solution, but I don’t like the style of export/extern.

After some code studying, I found out that even no direct calling @cImport() in client code, the .addIncludePath() must be called from the client exe target, as long as the the source file/package (which is @imported() by client code) contains some @cImport().

@jmc shows two possible ways of usage with the limits of zig.
Thanks!

Thanks @Sze, and sorry my statement was not that clear.
The early post described the project structure shows the problem scenario.

My supposed library is some c code (croot/) wrapped by zig (main.zig),
and the zig wrapper file include @cimport(@include()).

So the question is how the client code demo.zig uses the library without knowing the c code.

As I wrote, you at least need to tell zig how the function declaration looks, how is zig supposed to call a function if it doesn’t know it exists?
Usually header files with declarations are used exactly for that: expect this function with these parameters and return value

That is also why I would expect an .h file along with the static library .a file.
Could you hardcode that manually into your exe? Sure.

What puzzles me, is why you would want to, the header file info used with @cImport gets compiled into your exe anyway, so doing it manually doesn’t give you any improved “security” / “obscurity” / “code hiding”.

If you don’t like the .h file you could put the @cImport part in a separate build step and compile it to another object file and then add that to the final exe build. (Some of the details here would require me to do some experiments to know more about it)
Just like the static library which can be thought of as a big object file.

But I wouldn’t call that hiding the code, you just make it less appealing to look through and more work to reverse engineer.
It may not be recognizable as c/zig code anymore but it still is code.

I planned to build a zig library which wraps the c code behind a zig interface, so other exe or library artifacts can call the zig interface with the wrapping library more conveniently than calling the c interface directly.

That’s almost what I do.
I build it as a library.

Ok, I got what you thought.