#Export llvm-c to wasm using zig?
1 messages ยท Page 1 of 1 (latest)
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
It's also a nice showcase of an interesting use case for opaque as a namespace
I'm currently in the process of building llvm from source and lld
Cool, good luck โ if this is your first time, make sure you follow the steps from option A in the wiki as closely as you can: https://github.com/ziglang/zig/wiki/Building-Zig-From-Source#option-a-use-your-system-installed-build-tools
Don't use option B!
got it
@frank pasture Okay I got zig installed today. Can I ask for some help?
Go for it ๐
Always ok to just ask and someone will come around to answer asynchronously
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?
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.
But TLDR, this is where pub usingnamespace shines
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");
});
No problem, I might be unavailable for the next hour but then happy to jump back in
Thank you so much
src/main.zig:1:20: error: C import failed
pub usingnamespace @cImport({
^~~~~~~~
Looks like the cimport failed
Ok, I'm back. Is there more error information about the failed cImport?
This looks generally correct, have you set your include paths in build.zig? If you're using the build system, that is
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
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.
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");
}
b.addIncludePath()?
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
Yep, that adds another search path for C headers
Perfect! That's exactly what I need
an "include path" == a search path for headers
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.
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
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)))
Yes, it should. Is the pub usingnamespace part in main.zig?
Yeah it's outputting nothing ๐ฆ
should I be using a .so file instead?
Is llvm.wasm all of LLVM built to wasm?
in llvm.wasm?
OHH I get it, llvm.wasm is the out file name
Yeah sorry I didn't understand your question
I think what you want to do is statically link against the LLVM libs
(I was being unclear)
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.
I can't count the number of times I had to do this lol
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.
@pliant peak
zig build-obj \
-dynamic\
-O ReleaseSmall \
-target wasm32-wasi \
--library c \
-freference-trace \
-I /usr/include/llvm-c-15/ \
src/main.zig
-dynamic only works with build-lib AFAIK.
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
No I'm trying to build a CLI in node.js
I see
@pliant peak it looks like it built something big, but it didn't export any of the llvm functions
If you want the functions to be exported, then you must ask them to be, I think.
ie. export fn.
(Not to be confused with extern fn.)
Can I use @export() with a loop over the type?
I believe so.
May I have some help with that?
Sure.
I just started my zig journey today
Jumping in at the ocean-end, eh. ๐
๐
I admire your verasity.
Should I use @Type or @typeInfo
Obviously you have balls the size of skyscrapers.
If you want to iterate over the fields of a type, then the latter.
First though, I should see the code you're using.
Okay what would the loop look like?
Iterating over fields:
inline for (std.meta.fields(T)) |field| {
}
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
AH. ๐คฃ
๐
So.
Here's the crash course.
cImport generates extern declarations. (Think forward declarations in C.)
Oh that makes sense
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 ๐คฃ
Thank you so much for the info.
So I need to export each function?
or rather, I can @export each one.
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."
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. ๐
Okay.
If you're not seeing the symbols being exported, then I'm guessing that you do.
How can I loop over all the functions in a cImport
inline for (std.meta.fields(llvm)) |field| {
}
Each @cImport, just like @import, will return a struct that is full of declarations.
You'll want const c = @cImport(...);.
Yep llvm in my case
And then inline for (std.meta.declarations(c)) |decl| I think.
Fields are a different thing.
Yep! I was copying what @frank pasture did. But yeah declarations seems correct
Next I need to @export
Silly question but what will the new Zig code add over just using LLVM built for wasm?
The example on the website shows:
comptime {
@export(internalName, .{ .name = "foo", .linkage = .Strong });
}
Since llvm-c already exports the symbols and defines the code
Then, you can do something like:
@export(@field(c, decl.name), .{ .name = decl.name, .linkage = .Strong });
The ability to access these LLVM functions from outside the WASM module.
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 });
^~~~~~~~~~~~~~~~~~~~
I see, I think I'm still trying to wrap my head around the end product here
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
Ah yes. Seems like you want something like this instead:
inline for (std.meta.declarations(@This())) |decl| {
const d = @field(llvm, decl.name);
if (@typeInfo(@TypeOf(d)) == .Fn) {
@export(d, .{ .name = decl.name, .linkage = .Strong });
}
}
@this?
llvm in your case ๐
Remember I said before how @cImport returns a struct full of decls?
Well, that's because a file is actually just a struct. ๐
not sure how that works
@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 ๐
The current container type that is.
Maximum brain. ๐ง
Well, you cannot export a type, because.. well.. types don't exist at runtime ๐
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
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.
okay how can I identify inline functions?
That's probably a macro that cImport translated.
You can work around it for now with typeinfo, same as I was doing in my example for functions as well.
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 });
},
}
}
}
the switch needs a default case...
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
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,
};
};
yeah I just have no idea what to filter, what properties to inspect at all
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.
oh my god
control flow in inline for works fine now afaik
Hope so ๐คฃ
That's correct. ๐
oh yeah keyword logical ops feel weird at first but there's logic behind it
There's or too, if you need it.
the idea is that in zig, only keywords can do control flow
and and, or are short-circuiting, thus they are control flow
okay
takes a bit to get used to but rest assured you'll start accidentally doing it in c++ eventually :):)
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 => {}
}
}
}
}
A good start.
src/main.zig:42:33: error: TODO implement exporting arbitrary Value objects
@export(d, .{ .name = decl.name, .linkage = .Strong });
You're not doing that for loop that I had in my example though ๐
you can do an @compileLog(d) above that line to try and figure out what it's exporting
@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
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 });
}
}
but yeah here it'll hopefully be able to give you some insight as to where it's breaking
You're missing the outer: inline for, but yeah - otherwise seems reasonable.
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
ah yeah
mine also says params actually
I'm on 0.11.0-dev.1575+289e8fab7, ftr.
just updated and still does
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
๐คฃ
I suppose --- considering what you're trying to do ---- that would make sense ๐
@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)
I happen to be using the 0.10.1 binary
ah okay
I haven't built from source because my computer is a potato
if you hit any actual bugs it might be worth trying a nightly binary, since progress is faiiirly fast
you don't need to! there are master binaries on the website
Oh great
if you're on linux i can give you a handy dandy little script that just installs the latest zig tarball to ~/.opt/zig/
Yeah that would be awesome but not now
how do I do the compile time check for startsWith
on this: your latest thing doesn't actually filter on callconv, you should probably do that
if (comptime std.mem.startsWith(u8, fi.name, "LLVM")) { ... }
if (fi.calling_convention == .Inline) continue;
if (!std.mem.startsWith(u8, decl.name, "LLVM")) continue;
needs to have comptime specified
wait no
we're in a comptime block
ignore me!
Beat me to it ๐
sure, or to cover all bases, if (fi.calling_convention != .C) continue;
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 {
^~~~~
Ah.
lolwat
hang on
This is the difference between these things:
fn LLVMFooBar();
pub fn LLVMFooBar();
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
(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.)
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
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. ๐คฃ
๐
Problem is... you can't really not touch them if you're iterating over them. ๐ค
So I imagine I should probably filter out the name
Something like that may be necessary, yeah.
Honestly, you can probably remove anything beginning with __.
const d = @field(llvm, decl.name);
this is the problem
lol
if (!std.mem.startsWith(u8, decl.name, "LLVM")) continue;
Yeah - you'll have to filter it out before that.
Okay the next problem we hit
GIVE IT TO ME
/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?
I don't think so(?)
๐ฆ
zig build-lib \
-dynamic\
-O ReleaseSmall \
-target wasm32-wasi \
--library c \
-freference-trace \
-I /usr/include/llvm-c-15/ \
src/main.zig
Yeah - -I just provides a path for cImport to look in for the headers.
do I need the .o files?
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.
so it would result in me having to grab the .o files built for wasm
You'd have to build LLVM targeting WASM, yeah.
Okay I need to stop here.
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.
Nah I've been practicing building llvm from scratch to wasm
๐คฃ
practicing
lol
@pliant peak you're awesome. thank you so much for your help
you too @vivid hemlock
np
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.
Happy to help o7 ๐
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.
I'm not accessing the filesystem and emscripten is the compiler targeting wasm
So
Maybe emscripten can target wasi anyway
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.
@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.
@vivid hemlock mind if I have that command lol
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
oh yeah, it has to parse a small json manifest using jq
tyvm
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} }})
Can we dynamically create functions that call the c function?
There's no way to procedurally create code (function bodies) at comptime, only types and simple values
I'm taking a look at the TODO in Sema and it looks deceptively simple
could i see all the code for this?
i might have implemented the thing in sema (literally 5 lines) but idk how to trigger the case in order to test it
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
You just need a fi.calling_convention == .C
@pliant peak or @frank pasture can I make a file at compile time?
Using a comptime block
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
@frank pasture I need all the types from the c library at build time to generate typescript bindings
That's fine at build time, just not at comptime
Is there a way to gather all the exports from main.zig? (At build time)
No, build-time is before comptime so none of the file is analyzed or anything
@vivid hemlock suppose the c function re-export works. Would it be possible to make some kind of file with the reflected type info?
Could write a program that just generates that. ๐ค
I could try to use clang to dump the ast and the generate the bindings that way
You do like to jump in at the deep at end don't ya.
I respect it.
Bahahaha
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.
I can't generate a file at comptime
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.
So 2 compilations lol
This data's not changing.
Why bother making a whole thing if a quick and dirty approach is perfectly fine ๐
Yeah I don't mind quick and dirty.
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.
Yeah. Then actually execute itm
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.
I don't mean to sound impatient. I'm just doing some project planning. Will this be available on the main branch at some point when it gets tested?
This is perfect thank you
It trips up if the file contains const std = @import("std");, if that helps.
(And then use @This() at the iteration target of course.)
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
Why are we exporting them again? We only need to generate bindings with this, right?
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?
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
i'm not really at all familiar with wasm as a target, so forgive me being slow here; does re-exporting them in a wasm binary Just Work for achieving that?
Compiling LLVM to Wasm is the easy part
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
@crimson nebula perhaps we could generate the zig bindings automatically by creating a zig file too?
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
What do you mean?
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
Isn't our primary issue actually writing the bindings themselves into a file?
We need to generate two files. I'm on it ๐
We don't need to export anything
We just need to take all those headers, libLLVM*.a, and compile them together into a .wasm/.mjs file
...right?
Well we could stop at the wasm level
We can set the functions we need to export via Emscripten
I think you should have a little faith in me ๐
but we need Zig to accumulate all that delicious type info
Okay, fineeeee
Let me just confirm: You want to compile a WASM module that contains LLVM compiled for WASM, and then call those LLVM functions from outside the WASM module?
And the point of generating these bindings is because you do not want to have to bind all those LLVM functions manually.
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.
My game plan so far:
- make a file called
src/llvm.wasmwhich has all the cImports - use type reflection like before to write
src/lib.zigsrc/lib.mjsandsrc/lib.d.tsfrom a native program that simply loops over the function names and the parameters calledsrc/build-bindings.zig
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");
alright so we can use zig translate-c foo.j -target wasm32-unknown-unknown
(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.
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.
zig targets gets you some JSON output from which you can determine all valid combinations.
@pliant peak can I target wasm32-emscripten... i don't know what the target would be
I don't think so, as Emscripten's job is the same as what WASI is meant to do, only more jank ๐
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.
I don't want to access files
I am writing stuff from node.js which is reading the files and parsing AST
Well - maybe you don't - but LLVM normally provides procs that do, I think.
Like LLVMEmitObjectToFile, or whatever its called.
Yeah we won't be calling that function lol
we will emit the object to bitcode and link it manually using wasm-ld
If LLVM defines it, and it gets compiled to WASM when you compile LLVM, then it probably cause you problems ๐
okay so wasi it is!
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 ๐
How so?
Also again - just to be clear - this problem about talking to the filesystem is something that the compiled WASM module that contains LLVM will want/need, if you then try to use that module in a program (or make it into a WASM exe) later.
our goal is to make a compiler using JS
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