#Export llvm-c to wasm using zig?

1 messages ยท Page 1 of 1 (latest)

peak gulch
#

Hey! Let's say I want to export every (almost) llvm-c function inside a web assembly module using zig. Does anyone have any advice on how someone might do this?

frank pasture
#

I can't be of much help on the JS/wasm side yet, but in case you haven't seen it, the Zig code base has a pretty convenient handwritten Zig layer on top of llvm-c. I copied this approach in my latest project and it's much nicer than using LLVM via @cImport. https://github.com/ziglang/zig/blob/master/src/codegen/llvm/bindings.zig

Not sure if it will help you with the wasm side but thought I would share that hidden gem

peak gulch
#

WOW

#

Okay

frank pasture
#

It's also a nice showcase of an interesting use case for opaque as a namespace

peak gulch
#

I'm currently in the process of building llvm from source and lld

frank pasture
peak gulch
#

got it

peak gulch
#

@frank pasture Okay I got zig installed today. Can I ask for some help?

frank pasture
#

Go for it ๐Ÿ™‚

#

Always ok to just ask and someone will come around to answer asynchronously

peak gulch
#

Okay. How can I iterate over all the header file contents and export each function?

#

(and enum)

#

Do I have to write a layer of functions over the @cImports?

#

I'm sure it will involve @typeInfo

#

Better. Should I just copy and paste the codegen for llvm?

#

I assume I need to use this?

frank pasture
#

There are a few different directions you could take here I think. @cImport is one way, and what this does is automatically call zig translate-c on the headers (puts it in zig-cache) and declare a bunch of extern fn s etc. Then you could use pub usingnamespace to make those symbols all top-level.

You could also just call zig translate-c directly, then hand-edit the resulting file. This gives you a chance to improve the API, especially turning [*c]T into [*]T, ?*T etc.

frank pasture
peak gulch
#

Can you show me an example of what the imports would look like?

I think I also might like an example of what each function would look like.

I'm a zig beginner.

#

I apologize for need such granular help, but I plan on compiling all these functions to web assembly and publishing it.

#

Oh wait!

#
pub usingnamespace @cImport({
    @cInclude("epoxy/gl.h");
    @cInclude("GLFW/glfw3.h");
    @cDefine("STBI_ONLY_PNG", "");
    @cDefine("STBI_NO_STDIO", "");
    @cInclude("stb_image.h");
});
frank pasture
#

No problem, I might be unavailable for the next hour but then happy to jump back in

peak gulch
#

Thank you so much

peak gulch
#
src/main.zig:1:20: error: C import failed
pub usingnamespace @cImport({
                   ^~~~~~~~
#

Looks like the cimport failed

frank pasture
#

Ok, I'm back. Is there more error information about the failed cImport?

frank pasture
peak gulch
#

Thank you for the quick responses. I have all the header files in my /usr/include/llvm-c-15/llvm-c/ directory

#

I have no idea what I'm doing lol

frank pasture
#

You'll get there. So when you include a C header, zig needs to know where to find them. Same as if you are compiling a C program with gcc/clang. By default all of these will know to look relative to the C (or Zig) file that does the include. And they usually have some system-wide defaults like /usr/include. But LLVM is at a special path as you can see.

peak gulch
#

Okay. I can use the -I flag

#

Is it better to configure the builder?

frank pasture
#

So for example in my build.zig, I've added a special case for Homebrew-installed LLVM:

    if (builtin.os.tag == .macos) {
        exe.addIncludePath("/usr/local/opt/llvm/include");
        exe.addLibraryPath("/usr/local/opt/llvm/lib");
    }
peak gulch
#

b.addIncludePath()?

frank pasture
# peak gulch Okay. I can use the -I flag

The -I flag is definitely what you'd use if invoking zig build-exe directly, but if you use a build.zig, you can use the functions I pasted above, which internally is translated to those -I / -L flags

frank pasture
peak gulch
#

Perfect! That's exactly what I need

frank pasture
#

an "include path" == a search path for headers

peak gulch
#

I've had a massive crash course in nearly 4 programming languages this week outside of my job, so it's been a nightmare

#

You've been an awesome help.

frank pasture
#

Holy crap, that's a lot of stuff to learn in a week

#

Glad to help! Build tool stuff like this is hard in its own way, separate from languages and programming, heh

peak gulch
#

I added:

 b.addIncludePath("/usr/include/llvm-c-15/");
#

and now everything works

#

it compiles at least

#

Here's my zig command

#
zig build-obj \
  -O ReleaseSmall \
  -target wasm32-wasi \
  --library c \
  -freference-trace \
  -I /usr/include/llvm-c-15/ \
  src/main.zig
wasm-ld main.o -o llvm.wasm -O2 --no-entry --allow-undefined
#

It looks like the wasm file is empty

#
pub usingnamespace @cImport({
  @cInclude("llvm-c/Analysis.h");

So this is what I'm exporting ... just Analysis.h

Will this result in exports?

#
(module
  (memory (;0;) 2)
  (global (;0;) (mut i32) (i32.const 66560))
  (export "memory" (memory 0)))
frank pasture
#

Yes, it should. Is the pub usingnamespace part in main.zig?

peak gulch
#

Yeah it's outputting nothing ๐Ÿ˜ฆ

frank pasture
#

I have no idea how wasm-ld works, in case that's a factor

#

Let me see here

peak gulch
#

should I be using a .so file instead?

frank pasture
#

Is llvm.wasm all of LLVM built to wasm?

peak gulch
#

Heh. I'm just trying to expose the C api

#

those are c bindings afaicr

frank pasture
#

in llvm.wasm?

peak gulch
#

I'm trying to do as you describe

#

but the wasm file contains nothing

frank pasture
#

OHH I get it, llvm.wasm is the out file name

peak gulch
#

Yeah sorry I didn't understand your question

frank pasture
#

I think what you want to do is statically link against the LLVM libs

#

(I was being unclear)

peak gulch
#

I think you're right

#

Should I build llvm first by hand?

frank pasture
#

Bear with me as I wrap my head around how this would work. I think you need to build LLVM with llvm-c, from source, with wasm32 as the target.

peak gulch
#

I can't count the number of times I had to do this lol

pliant peak
#

You want zig build-lib -dynamic ...

#

.wasm files are .sos basically.

#

I'm also fairly certain that Zig does the WASM linking already, so you don't need to do that part separately.

#

If you want something that you can run with wastime or the like, then zig build-exe might actually just work for you.

peak gulch
#

@pliant peak

zig build-obj \
  -dynamic\
  -O ReleaseSmall \
  -target wasm32-wasi \
  --library c \
  -freference-trace \
  -I /usr/include/llvm-c-15/ \
  src/main.zig
pliant peak
#

-dynamic only works with build-lib AFAIK.

peak gulch
#

I don't need a wasmtime build. I forgot to change the command

#

OH it's working now

frank pasture
#

Are you planning to target web? Downloading a full llvm.wasm from a website might take a while ๐Ÿ™ƒ

#

Maybe not as bad if it only has the wasm target backend built in

peak gulch
#

No I'm trying to build a CLI in node.js

frank pasture
#

I see

peak gulch
#

@pliant peak it looks like it built something big, but it didn't export any of the llvm functions

pliant peak
#

ie. export fn.

#

(Not to be confused with extern fn.)

peak gulch
#

Can I use @export() with a loop over the type?

pliant peak
#

I believe so.

peak gulch
#

May I have some help with that?

pliant peak
#

Sure.

peak gulch
#

I just started my zig journey today

pliant peak
#

Jumping in at the ocean-end, eh. ๐Ÿ˜„

peak gulch
#

๐Ÿ˜‚

pliant peak
#

I admire your verasity.

peak gulch
#

Should I use @Type or @typeInfo

pliant peak
#

Obviously you have balls the size of skyscrapers.

pliant peak
#

First though, I should see the code you're using.

peak gulch
#

Okay what would the loop look like?

pliant peak
peak gulch
#
pub usingnamespace @cImport({
  @cInclude("llvm-c/Analysis.h");
  @cInclude("llvm-c/Core.h");
  @cInclude("llvm-c/Disassembler.h");
  @cInclude("llvm-c/ExecutionEngine.h");
  @cInclude("llvm-c/LLJIT.h");
  @cInclude("llvm-c/OrcEE.h");
  @cInclude("llvm-c/TargetMachine.h");
  @cInclude("llvm-c/lto.h");
  @cInclude("llvm-c/BitReader.h");
  @cInclude("llvm-c/DataTypes.h");
  @cInclude("llvm-c/DisassemblerTypes.h");
  @cInclude("llvm-c/ExternC.h");
  @cInclude("llvm-c/Linker.h");
  @cInclude("llvm-c/Remarks.h");
  @cInclude("llvm-c/BitWriter.h");
  @cInclude("llvm-c/DebugInfo.h");
  @cInclude("llvm-c/Error.h");
  @cInclude("llvm-c/IRReader.h");
  @cInclude("llvm-c/Object.h");
  @cInclude("llvm-c/Support.h");
  @cInclude("llvm-c/Types.h");
  @cInclude("llvm-c/Comdat.h");
  @cInclude("llvm-c/Deprecated.h");
  @cInclude("llvm-c/ErrorHandling.h");
  @cInclude("llvm-c/Initialization.h");
  @cInclude("llvm-c/Orc.h");
  @cInclude("llvm-c/Target.h");
  @cInclude("llvm-c/blake3.h");
});
#

this is what I have so far

pliant peak
#

AH. ๐Ÿคฃ

peak gulch
#

๐Ÿ™ƒ

pliant peak
#

So.

#

Here's the crash course.

#

cImport generates extern declarations. (Think forward declarations in C.)

peak gulch
#

Oh that makes sense

pliant peak
#

These are just how you declare the existence of things that aren't in your compilation unit.

#

It also does its best to declare Zig types that have the same layout as every C type in the headers.

#

These externs only require that the other compilation units are linked in the end.

#

If those symbols are exported from C -- i.e with extern "C" -- then they'll also be available IIRC from the final WASM -- at least if it works like an SO.
(If it's C code, and not C++, then extern "C" is unnecessary, because everything is exported by default.)

#

At least, I think that's how it works - been a while since I juggled this kinda things ๐Ÿคฃ

peak gulch
#

Thank you so much for the info.

#

So I need to export each function?

#

or rather, I can @export each one.

pliant peak
#

So, it depends. ๐Ÿ˜„

#

export means, "I want this thing to be publically exported from the Zig compilation unit, and ultimately, the final output - whatever that is."

peak gulch
#

Yes of course.

#

I think I understand that.

pliant peak
#

So, if you make a SO, then these will be things that can be accessed by anything that links that SO.

#

Likewise, I would imagine with .wasm, since they are the WASM equiv to an SO.

#

I'm not sure if you actually need to do this or not in this case, but if you do, then you do. ๐Ÿ˜„

peak gulch
#

Okay.

pliant peak
#

If you're not seeing the symbols being exported, then I'm guessing that you do.

peak gulch
#

How can I loop over all the functions in a cImport

#
inline for (std.meta.fields(llvm)) |field| {

}
pliant peak
#

Each @cImport, just like @import, will return a struct that is full of declarations.

#

You'll want const c = @cImport(...);.

peak gulch
#

Yep llvm in my case

pliant peak
#

And then inline for (std.meta.declarations(c)) |decl| I think.

#

Fields are a different thing.

peak gulch
#

Yep! I was copying what @frank pasture did. But yeah declarations seems correct

#

Next I need to @export

frank pasture
#

Silly question but what will the new Zig code add over just using LLVM built for wasm?

peak gulch
#

The example on the website shows:

comptime {
    @export(internalName, .{ .name = "foo", .linkage = .Strong });
}

frank pasture
#

Since llvm-c already exports the symbols and defines the code

pliant peak
#

Then, you can do something like:

@export(@field(c, decl.name), .{ .name = decl.name, .linkage = .Strong });
pliant peak
peak gulch
#

Thank you so much! I'll give it a shot

#
const std = @import("std");
const llvm = @cImport({
  @cInclude("llvm-c/Analysis.h");
  @cInclude("llvm-c/Core.h");
  @cInclude("llvm-c/Disassembler.h");
  @cInclude("llvm-c/ExecutionEngine.h");
  @cInclude("llvm-c/LLJIT.h");
  @cInclude("llvm-c/OrcEE.h");
  @cInclude("llvm-c/TargetMachine.h");
  @cInclude("llvm-c/lto.h");
  @cInclude("llvm-c/BitReader.h");
  @cInclude("llvm-c/DataTypes.h");
  @cInclude("llvm-c/DisassemblerTypes.h");
  @cInclude("llvm-c/ExternC.h");
  @cInclude("llvm-c/Linker.h");
  @cInclude("llvm-c/Remarks.h");
  @cInclude("llvm-c/BitWriter.h");
  @cInclude("llvm-c/DebugInfo.h");
  @cInclude("llvm-c/Error.h");
  @cInclude("llvm-c/IRReader.h");
  @cInclude("llvm-c/Object.h");
  @cInclude("llvm-c/Support.h");
  @cInclude("llvm-c/Types.h");
  @cInclude("llvm-c/Comdat.h");
  @cInclude("llvm-c/Deprecated.h");
  @cInclude("llvm-c/ErrorHandling.h");
  @cInclude("llvm-c/Initialization.h");
  @cInclude("llvm-c/Orc.h");
  @cInclude("llvm-c/Target.h");
  @cInclude("llvm-c/blake3.h");
});

comptime {
    inline for (std.meta.declarations(llvm)) |decl| {
        @export(@field(c, decl.name), .{ .name = decl.name, .linkage = .Strong });
    }
}
#

Looks like

src/main.zig:35:17: error: symbol to export must identify a declaration
        @export(@field(c, decl.name), .{ .name = decl.name, .linkage = .Strong });
                ^~~~~~~~~~~~~~~~~~~~
frank pasture
peak gulch
#

The end product is LLVM functions that node.js can call

#

I can do something like rust's inkwell

#

@pliant peak almost there. Just need to fix the identify this here

pliant peak
peak gulch
#

@this?

pliant peak
#

llvm in your case ๐Ÿ˜„

peak gulch
#

shoudl @This be llvm?

#

Oh

#

I might like to export the enums too

pliant peak
#

Remember I said before how @cImport returns a struct full of decls?
Well, that's because a file is actually just a struct. ๐Ÿ˜„

peak gulch
#

not sure how that works

pliant peak
#

@This() just returns the current container - whatever struct etc that is.

#

Which is what I was using for testing what you needed on my side ๐Ÿ˜„

peak gulch
#

Oh okay

#

See that makes sense

pliant peak
pliant peak
pliant peak
peak gulch
#
src/main.zig:37:13: error: unable to export type 'fn(u16) callconv(.Inline) u16'
            @export(d, .{ .name = decl.name, .linkage = .Strong });
            ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main.zig:37:13: note: inline function cannot be extern
#

Now this is really cool

pliant peak
#

Remember how I said that importing just generates Zig types that have the same layout as the C types? That's why. Because the C types are not accessible across the linking barrier.

peak gulch
#

okay how can I identify inline functions?

pliant peak
#

So:

#
inline for (std.meta.declarations(@This())) |decl| {
    const d = @field(llvm, decl.name);
    const ti = @typeInfo(@TypeOf(d));
    if (ti == .Fn) {
        switch (ti.Fn.calling_convention) {
            // NOTE: may only need to check .C, but still.
            .C, .Stdcall, .SysV, .Win64 => {
                @export(d, .{ .name = decl.name, .linkage = .Strong });
            },
        }
    }
}

peak gulch
#

the switch needs a default case...

pliant peak
#

else => {},

#

Serves me right for not trying to run it ๐Ÿคฃ

peak gulch
#
src/main.zig:41:29: error: TODO implement exporting arbitrary Value objects
                    @export(d, .{ .name = decl.name, .linkage = .Strong });
#

๐Ÿคฆโ€โ™‚๏ธ

#

You weren't kidding. this is right off the deep end LOL

pliant peak
#

I'm unsure exactly what type of value it is complaining about, but you basically just have to only export things with types that it can do for now ๐Ÿ˜„

#

This means adding more filters until it works ๐Ÿ˜„

#

Here is what the typeinfo for a function looks like:

    pub const Fn = struct {
        calling_convention: CallingConvention,
        alignment: comptime_int,
        is_generic: bool,
        is_var_args: bool,
        /// TODO change the language spec to make this not optional.
        return_type: ?type,
        params: []const Param,

        /// This data structure is used by the Zig language code generation and
        /// therefore must be kept in sync with the compiler implementation.
        pub const Param = struct {
            is_generic: bool,
            is_noalias: bool,
            type: ?type,
        };
    };

peak gulch
#

yeah I just have no idea what to filter, what properties to inspect at all

pliant peak
#

I would probably skip anything where is_generic is true, or is_var_args is true. ๐Ÿ˜„

#

Or where return_type == null.

#

Likewise for any of the parameters, probably.

#

Something like this may work for you:

outer: inline for (...) |decl| {
    const d = @field(llvm, decl.name);
    const ti = @typeInfo(@TypeOf(d));
    if (ti != .Fn) continue;

    const fi = ti.Fn;
    if (fi.return_type == null) continue;
    if (fi.is_generic) continue;
    if (fi.is_var_args) continue;
    for (fi.params) |param| {
        if (param.is_generic) continue :outer;
        if (param.type == null) continue :outer;
    }

    @export(...);
}
#

There used to be a bug with continue/break in an inline for loop. If that's still present, then you'll have to just use nested ifs instead.

peak gulch
#

oh my god

vivid hemlock
#

control flow in inline for works fine now afaik

peak gulch
#

and

#

not &&

pliant peak
pliant peak
vivid hemlock
# peak gulch `and`

oh yeah keyword logical ops feel weird at first but there's logic behind it

pliant peak
#

There's or too, if you need it.

vivid hemlock
#

the idea is that in zig, only keywords can do control flow

#

and and, or are short-circuiting, thus they are control flow

peak gulch
#

okay

vivid hemlock
#

takes a bit to get used to but rest assured you'll start accidentally doing it in c++ eventually :):)

peak gulch
#

comptime {
    inline for (std.meta.declarations(llvm)) |decl| {
        const d = @field(llvm, decl.name);
        const ti = @typeInfo(@TypeOf(d));
        if (ti == .Fn) {
            switch (ti.Fn.calling_convention) {
                // NOTE: may only need to check .C, but still.
                .C, .Stdcall, .SysV, .Win64 => {
                    if (!ti.Fn.is_generic and !ti.Fn.is_var_args and ti.Fn.return_type != null) {
                        @export(d, .{ .name = decl.name, .linkage = .Strong });
                    }
                },
                else => {}
            }
        }
    }
}
pliant peak
#

A good start.

peak gulch
#
src/main.zig:42:33: error: TODO implement exporting arbitrary Value objects
                        @export(d, .{ .name = decl.name, .linkage = .Strong });
pliant peak
#

You're not doing that for loop that I had in my example though ๐Ÿ˜‰

vivid hemlock
#

you can do an @compileLog(d) above that line to try and figure out what it's exporting

pliant peak
#

Ah yes! The compile-time printf!

#

Well - ish.

#

More like println.

vivid hemlock
#

@compileLog is weird but handy

#

it does its darn best to output any value in a comprehensible format, which is very handy! what's less handy is that it doesn't print computed strings as strings so you can't use std.fmt.comptimePrint with it

peak gulch
#

comptime {
    inline for (std.meta.declarations(llvm)) |decl| {
        const d = @field(llvm, decl.name);
        const ti = @typeInfo(@TypeOf(d));
        if (ti != .Fn) continue;

        const fi = ti.Fn;
        if (fi.return_type == null) continue;
        if (fi.is_generic) continue;
        if (fi.is_var_args) continue;
        for (fi.params) |param| {
            if (param.is_generic) continue :outer;
            if (param.type == null) continue :outer;
        }
        @compileLog(d);
        @export(d, .{ .name = decl.name, .linkage = .Strong });
    }
}
vivid hemlock
#

but yeah here it'll hopefully be able to give you some insight as to where it's breaking

pliant peak
peak gulch
#

Oh yeah of course

#

So apparently the struct doesn't have a params field?

#
pub const Fn = struct {
        calling_convention: CallingConvention,
        alignment: comptime_int,
        is_generic: bool,
        is_var_args: bool,
        /// TODO change the language spec to make this not optional.
        return_type: ?type,
        args: []const Param,

        /// This data structure is used by the Zig language code generation and
        /// therefore must be kept in sync with the compiler implementation.
        pub const Param = struct {
            is_generic: bool,
            is_noalias: bool,
            arg_type: ?type,
        }
#

args

#

lol

vivid hemlock
#

ah yeah

pliant peak
#

Huh. Mine says params ๐Ÿ˜„

#

Maybe that was changed recently.

vivid hemlock
#

mine also says params actually

pliant peak
#

I'm on 0.11.0-dev.1575+289e8fab7, ftr.

vivid hemlock
#

just updated and still does

peak gulch
#
Compile Log Output:
@as(fn(u16) callconv(.Inline) u16, (function '__builtin_bswap16'))
#

heh

#

I should probably filter by name

#

if the name begins with LLVM chances are I should export it

pliant peak
#

๐Ÿคฃ

#

I suppose --- considering what you're trying to do ---- that would make sense ๐Ÿ˜„

vivid hemlock
# vivid hemlock just updated and still does

@peak gulch It seems like that was changed in december, so your compiler is a fair bit out of date - might be worth updating
unless you're intentionally using 0.10.1 or something (haven't read backlog)

peak gulch
#

I happen to be using the 0.10.1 binary

vivid hemlock
#

ah okay

peak gulch
#

I haven't built from source because my computer is a potato

vivid hemlock
#

if you hit any actual bugs it might be worth trying a nightly binary, since progress is faiiirly fast

vivid hemlock
peak gulch
#

Oh great

vivid hemlock
#

if you're on linux i can give you a handy dandy little script that just installs the latest zig tarball to ~/.opt/zig/

peak gulch
#

Yeah that would be awesome but not now

#

how do I do the compile time check for startsWith

vivid hemlock
peak gulch
vivid hemlock
peak gulch
#
        if (fi.calling_convention == .Inline) continue;
pliant peak
#
if (!std.mem.startsWith(u8, decl.name, "LLVM")) continue;
vivid hemlock
#

wait no

#

we're in a comptime block

#

ignore me!

pliant peak
#

Beat me to it ๐Ÿ˜›

vivid hemlock
peak gulch
#
src/main.zig:35:19: error: 'union_unnamed_1' is not marked 'pub'
        const d = @field(llvm, decl.name);
                  ^~~~~~~~~~~~~~~~~~~~~~~
/home/jtenner/llvm-zig/zig-cache/o/b98892c84fe7eeec1f62f307de5034c5/cimport.zig:136:1: note: declared here
const union_unnamed_1 = extern union {
^~~~~
peak gulch
#

lolwat

pliant peak
#

You'll want to add this too:

#
if (!decl.is_pub) continue;
#

๐Ÿ˜„

peak gulch
#

hang on

pliant peak
#

This is the difference between these things:

fn LLVMFooBar();
pub fn LLVMFooBar();
vivid hemlock
#

it's arguably a bug that you can see the non-pub decls at all, but for now you can, so yeah just gotta skip em

pliant peak
#

(pub means "Can be accessed outside the current file", but only semantics-wise in Zig source code. i.e: it's not a linking thing.)

peak gulch
#

Okay so I added that

#
/home/jtenner/llvm-zig/zig-cache/o/b98892c84fe7eeec1f62f307de5034c5/cimport.zig:320:33: error: unable to translate macro: undefined identifier `LL`
pub const __INTMAX_C_SUFFIX__ = @compileError("unable to translate macro: undefined identifier `LL`"); // (no file):82:9
                                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    comptime_0: src/main.zig:37:19
pliant peak
#

Ah.

#

This may break you. ๐Ÿ˜„

#

So...

#

The way that cImport handles macros that it cannot make sense of, is to make them @compileError calls.

#

This means that they cause a compile error if they are touched.

#

Guess what - you're touching em. ๐Ÿคฃ

peak gulch
#

๐Ÿ™ƒ

pliant peak
#

Problem is... you can't really not touch them if you're iterating over them. ๐Ÿค”

peak gulch
#

So I imagine I should probably filter out the name

pliant peak
#

Something like that may be necessary, yeah.

#

Honestly, you can probably remove anything beginning with __.

peak gulch
#
const d = @field(llvm, decl.name);
#

this is the problem

#

lol

#
if (!std.mem.startsWith(u8, decl.name, "LLVM")) continue;
pliant peak
#

Yeah - you'll have to filter it out before that.

peak gulch
#

Okay the next problem we hit

pliant peak
#

GIVE IT TO ME

peak gulch
#
/home/jtenner/llvm-zig/zig-cache/o/b98892c84fe7eeec1f62f307de5034c5/cimport.zig:325:44: error: unable to translate macro: undefined identifier `_Pragma`
pub const LLVM_C_STRICT_PROTOTYPES_BEGIN = @compileError("unable to translate macro: undefined identifier `_Pragma`"); // /usr/include/llvm-c-15/llvm-c/ExternC.h:18:9
                                           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    comptime_0: src/main.zig:37:19
#

is there any way to determine if it's a const?

pliant peak
#

I don't think so(?)

peak gulch
#

๐Ÿ˜ฆ

pliant peak
#

Oh hold on a minute... ๐Ÿ‘€

#

In your build command, you didn't link LLVM...

peak gulch
#
zig build-lib \
  -dynamic\
  -O ReleaseSmall \
  -target wasm32-wasi \
  --library c \
  -freference-trace \
  -I /usr/include/llvm-c-15/ \
  src/main.zig
pliant peak
#

Yeah - -I just provides a path for cImport to look in for the headers.

peak gulch
#

do I need the .o files?

pliant peak
#

It doesn't actually ask it to link or build anything, I don't think.

#

So

#

What you need is to build LLVM with Clang, or Zig.

#

Then link the result here.

peak gulch
#

so it would result in me having to grab the .o files built for wasm

pliant peak
#

You'd have to build LLVM targeting WASM, yeah.

peak gulch
#

Okay I need to stop here.

pliant peak
#

Now, you can do that with build.zig -- but that would mean translating LLVM's build system to it.
The alternative is to build LLVM using CMake etc, targeting WASM, and then figure out how to link all that when you zig build-lib.

peak gulch
#

Nah I've been practicing building llvm from scratch to wasm

pliant peak
#

๐Ÿคฃ

peak gulch
#

practicing

#

lol

#

@pliant peak you're awesome. thank you so much for your help

#

you too @vivid hemlock

vivid hemlock
#

np

pliant peak
#

I think you'd have to do is something like:

sudo apt-get install cmake make
# clone LLVM
cd llvm
mkdir build
cd build
cmake .. -DCMAKE_C_COMPILER='zig cc' -DCMAKE_CXX_COMPILER='zig c++' -DCMAKE_TOOLCHAIN_FILE=FOOBAR
make

Not sure about the -DCMAKE_TOOLCHAIN_FILE, but I suspect that LLVM would have one of those somewhere in its repo. In which case, asuming it's what I think it is--you'd just find the one for WASM32-WASI if there is one.

pliant peak
peak gulch
#

I'm using emscripten

#

can I target wasm-unknown instead?

#

I don't think I need wasi

pliant peak
#

You can do wasm32-freestanding if that helps.

#

WASI is just the OS; the interface with the environment.

#

Freestanding just means all of that is entirely up to you, instead of having WASI's setup to fall back on.

peak gulch
#

I'm not accessing the filesystem and emscripten is the compiler targeting wasm

#

So

#

Maybe emscripten can target wasi anyway

pliant peak
#

I believe Emscripten generates the glue code required that WASI would normally do on the WASM side, but it implements a bunch of the stuff in JS, which of course isn't fast.
And it has to use a bunch of tricks in JS to make things work.
I've never used it successfully, but presumably that can work to some extent.

peak gulch
#

@pliant peak I have a ton of experience with web assembly from emscripten to AssemblyScript, so using those tools has been easier for me to do.

peak gulch
#

@vivid hemlock mind if I have that command lol

vivid hemlock
#

oh yeah, this is the script:

#!/bin/sh -e
case "$(uname -s)" in
    Linux) os=linux;;
    Darwin) os=macos;;
    FreeBSD) os=freebsd;;
    *) echo 'Unsupported OS' >&2; exit 1;;
esac
curl -sSL 'https://ziglang.org/download/index.json' | jq -r ".master.\"$(uname -m)-$os\" | .tarball" | {
    read -r url
    tmpdir="$(mktemp -d)"
    cd "$tmpdir"

    curl -Lo _zig.tar.xz "$url"
    tar xf _zig.tar.xz
    rm -rf "$HOME/.opt/zig"
    mv zig*/ "$HOME/.opt/zig"

    cd
    rm -rf "$tmpdir"
}
#

i just have that in my PATH and run it every few days

peak gulch
#

looks like it requires jq

vivid hemlock
#

oh yeah, it has to parse a small json manifest using jq

peak gulch
#

tyvm

peak gulch
#
jtenner@CTWS140:~/zig-llvm-c$ ./build.sh
error: wasm-ld: /home/jtenner/.cache/zig/o/59c0f4a779642e16c63da8374aaf3ca8/libc.a(/home/jtenner/.cache/zig/o/e734c541c5d6455c42b069419261e554/__main_void.o): undefined symbol: main
#

hey guys

#

I'm really close

#

looks like the linker can't find a main function

#

@pliant peak I'm still in the ocean lol

#

@vivid hemlock it looks like the script we wrote doesn't work

#

Yeah every function in core.h has the c calling convention

#
src/main.zig:60:17: error: TODO implement exporting arbitrary Value objects
        @export(d, .{ .name = decl.name, .linkage = .Strong });
                ^

Compile Log Output:
@as(builtin.Type.Fn, .{.calling_convention = .C, .alignment = 1, .is_generic = false, .is_var_args = false, .return_type = void, .params = .{ .{.is_generic = false, .is_noalias = false, .type = ?*const fn([*c]const u8) callconv(.C) void} }})
peak gulch
#

Can we dynamically create functions that call the c function?

frank pasture
#

There's no way to procedurally create code (function bodies) at comptime, only types and simple values

vivid hemlock
#

I'm taking a look at the TODO in Sema and it looks deceptively simple

vivid hemlock
#

i might have implemented the thing in sema (literally 5 lines) but idk how to trigger the case in order to test it

peak gulch
#

Just need a minute to get my bearings just woke up

#

comptime {
    @setEvalBranchQuota(1000000);

    outer: inline for (std.meta.declarations(llvm)) |decl| {
        if (!decl.is_pub) continue;
        if (std.mem.eql(u8, decl.name, "LLVM_ATTRIBUTE_C_DEPRECATED")) continue;
        if (std.mem.eql(u8, decl.name, "LLVM_C_STRICT_PROTOTYPES_BEGIN")) continue;
        if (std.mem.eql(u8, decl.name, "LLVM_C_STRICT_PROTOTYPES_END")) continue;
        if (std.mem.eql(u8, decl.name, "LLVM_FOR_EACH_VALUE_SUBCLASS")) continue;
        if (std.mem.eql(u8, decl.name, "LLVM_DECLARE_VALUE_CAST")) continue;
        if (std.mem.eql(u8, decl.name, "LLVM_C_EXTERN_C_BEGIN")) continue;
        if (std.mem.eql(u8, decl.name, "LLVM_C_EXTERN_C_END")) continue;
        if (!std.mem.startsWith(u8, decl.name, "LLVM")) continue;
        const d = @field(llvm, decl.name);
        const ti = @typeInfo(@TypeOf(d));
        if (ti != .Fn) continue;

        const fi = ti.Fn;
        if (fi.return_type == null) continue;
        if (fi.is_generic) continue;
        if (fi.is_var_args) continue;
        if (fi.calling_convention == .Inline) continue;
        // if (fi.calling_convention == .C) continue;
        for (fi.params) |param| {
            if (param.is_generic) continue :outer;
        }
        @compileLog(fi);

        @export(d, .{ .name = decl.name, .linkage = .Strong });
    }
}

export fn main() i32 {
    return 0;
}
#

You can ignore the LLVM stuff obviously

#

@vivid hemlock Thank you so much for your help

peak gulch
#

You just need a fi.calling_convention == .C

peak gulch
#

@pliant peak or @frank pasture can I make a file at compile time?

#

Using a comptime block

frank pasture
#

Comptime can't do I/O as far as I know, so I think no. You could do it as a step in build.zig but if from what I've seen in this thread I assume you don't plan to use the zig build system

peak gulch
#

@frank pasture I need all the types from the c library at build time to generate typescript bindings

frank pasture
#

That's fine at build time, just not at comptime

peak gulch
#

Is there a way to gather all the exports from main.zig? (At build time)

vivid hemlock
#

No, build-time is before comptime so none of the file is analyzed or anything

peak gulch
#

@vivid hemlock suppose the c function re-export works. Would it be possible to make some kind of file with the reflected type info?

pliant peak
peak gulch
#

I could try to use clang to dump the ast and the generate the bindings that way

pliant peak
#

I respect it.

peak gulch
#

Bahahaha

pliant peak
#

But if you can get that inline for loop working, that's probably the easier choice.

#

Use the resulting data to write out a file at runtime.

peak gulch
#

I can't generate a file at comptime

pliant peak
#

Then you can use that file at comptime in a future compilation.

#

No, I mean that you use it temporarily to generate what you need.

peak gulch
#

So 2 compilations lol

pliant peak
#

This data's not changing.

#

Why bother making a whole thing if a quick and dirty approach is perfectly fine ๐Ÿ˜„

peak gulch
#

Yeah I don't mind quick and dirty.

pliant peak
#

Besides, you can always make a separate program that imports the same things, does the inline loop, and generates it, and have that be a separate program.

peak gulch
#

Yeah. Then actually execute itm

pliant peak
#
zig run generate.zig
zig build-exe main.zig -target wasm32-wasi
#

You can use @embedFile to read a file at comptime.

#

You just can't write one.

peak gulch
peak gulch
pliant peak
#

(And then use @This() at the iteration target of course.)

vivid hemlock
#

Yeah sorry, haven't got around to testing yet, will do so soon

#

If it works I'll PR it but it might take a while to get in since the PR backlog is quite big rn

crimson nebula
vivid hemlock
#

Ah okay, so this issue is specifically caused by re-exporting extern fns

#

The question of why we're exporting them again is.... actually a good one lol. What's the end goal here?

peak gulch
#

We want to compile all the llvm-c headers to wasm

#

Then we can use zig to generate d.ts files so that things can be moderately type safe.

This is an alternative to use emscripten's embind

vivid hemlock
crimson nebula
#

but using the raw Wasm exports isn't good DX

#

And there are too many C functions for us to manually write bindings for (and we'd have to update them manually too)

#

So we opted for some sort of bindings generation

#

As @peak gulch can tell you, embind isn't all that cooperative

peak gulch
#

@crimson nebula perhaps we could generate the zig bindings automatically by creating a zig file too?

crimson nebula
#

In short, we need a way to discover all the functions, enums, and typedefs in a set of llvm-c headers and use that information to write JS/TS wrappers over llvm-c compiled to Wasm

peak gulch
#

Thank you for helping me explain that.

#

Currently zig doesn't like exporting the c functions via @export

#

However, we can generate a function in a zig file that actually calls those exported functions

crimson nebula
#

Isn't our primary issue actually writing the bindings themselves into a file?

peak gulch
#

We need to generate two files. I'm on it ๐Ÿ™‚

crimson nebula
peak gulch
#

One for the exported zig functions to wasm

#

One for the d.ts definitions

crimson nebula
#

We just need to take all those headers, libLLVM*.a, and compile them together into a .wasm/.mjs file

#

...right?

peak gulch
#

Well we could stop at the wasm level

crimson nebula
#

We can set the functions we need to export via Emscripten

peak gulch
#

I think you should have a little faith in me ๐Ÿ™‚

crimson nebula
#

but we need Zig to accumulate all that delicious type info

crimson nebula
pliant peak
#

And the point of generating these bindings is because you do not want to have to bind all those LLVM functions manually.

peak gulch
#

Bingo

#

Heh that whole sentence made my head spin lol

pliant peak
#

Okay, cool.

#

๐Ÿคฃ

#

FTR, you may find a complication about using @cImport from generate.zig, because that program will not be targeting WASM, however the cImport system takes that into account when translating macros and types, and things.

#

You can run cImport "manually" though, and specify the target to interpret it with.

peak gulch
#

My game plan so far:

  1. make a file called src/llvm.wasm which has all the cImports
  2. use type reflection like before to write src/lib.zig src/lib.mjs and src/lib.d.ts from a native program that simply loops over the function names and the parameters called src/build-bindings.zig
pliant peak
#

See, const llvm = @cImport("foo.h"); is the same as:

zig translate-c foo.h -target TARGET > zig-cache/cimport.zig

const llvm = @import("zig-cache/cimport.zig");

peak gulch
#

alright so we can use zig translate-c foo.j -target wasm32-unknown-unknown

pliant peak
#

(Well - it actually also passes -I ... and whathaveyou to translate-c too, but you get the idea.)

#

That's my thinking.

#

You can then try to import that file into generate.zig.

peak gulch
#

yes

#

what are all the targets for zig?

pliant peak
#

Now, that may not work; it depends on exactly what the file contains, etc. Things that are available on WASM are not on x64, and vice-versa. So you may find yourself having to manually delete some stuff from the translated file to make it work or whatever.

pliant peak
peak gulch
#

@pliant peak can I target wasm32-emscripten... i don't know what the target would be

pliant peak
peak gulch
#

so I have to target wasi?

#

I just want a library ๐Ÿ˜ญ

pliant peak
#

It depends largely on what LLVM wants to be able to do.
If that wants to be opening files and the like, then wasm32-freestanding doesn't provide anything for that, so it won't work.
WASI's job is that it standardizes what symbols are provided by the external environment for accessing files and whatnot, hence why I suggest it.

peak gulch
#

I don't want to access files

#

I am writing stuff from node.js which is reading the files and parsing AST

pliant peak
#

Well - maybe you don't - but LLVM normally provides procs that do, I think.

#

Like LLVMEmitObjectToFile, or whatever its called.

peak gulch
#

Yeah we won't be calling that function lol

#

we will emit the object to bitcode and link it manually using wasm-ld

pliant peak
#

If LLVM defines it, and it gets compiled to WASM when you compile LLVM, then it probably cause you problems ๐Ÿ˜„

peak gulch
#

okay so wasi it is!

pliant peak
#

It's possible that LLVM just omits that if it's compiled to WASM, but you'd have to examine what LLVM does there to find out definitively ๐Ÿ˜„

pliant peak
peak gulch
#

We already have a parser/ast, and we want to use llvm to generate bitcode

#

In the meantime, compiling LLVM to wasm as a library will help everyone who can consume wasm and wants to use llvm