I wrote a “middleman” when interacting with zig from mql5. The program that runs mql5 does not allow external debuggers.
So I made a code generator that takes the zig library and creates a shared library and server. Shared library can be linked to mql5, and debugger can interact with server.
It works by invoking generator in build.zig
:
const zig_mql_out = try zig_mql.generate(
@import("src/mql5.zig"),
"mql5_trading",
);
const gen_wf = b.addUpdateSourceFiles();
if (zig_mql_out.server) |server| {
_ = gen_wf.addBytesToSource(
server,
"generated/server.zig",
);
}
if (zig_mql_out.middleware) |middleware| {
_ = gen_wf.addBytesToSource(
middleware,
"generated/middleware.zig",
);
}
gen_debug_step.dependOn(&gen_wf.step);
Is there a way, to enclose zig_mql.generate
as dependency for build step? Generation is slow, so I want to call it only when I need to build server and shared library.
After looking into std.Build
sources, I think it’s possible to write needed build step by hand. The main thing to be implemented is makeFn
(see in std.Build.Step
), as I understand, most of the step logic lies here.
You probably don’t want to use custom build steps (makeFn
), they are more or less considered deprecated and will likely be removed in the future. Instead, what you should do is compile an executable that takes input/output paths as CLI arguments, and then invoke it with b.addRunArtifact()
, passing input arguments using Step.Run.addArg()
(use .addFileArg()
for files) and output file arguments using .addOutputFileArg()
. The latter will return a LazyPath
to the generated file, which you can then either pass to Step.UpdateSourceFiles.addCopyFileToSource()
to update a source file in place, or use as the root source file for a module that is imported by your main program.
See Zig Build System#Running the Project’s Tools for a complete example of how to generate code like this, but to give you a rough idea, you want a main function that looks something like this:
// generator.zig
pub fn main() !void {
const allocator = ...;
var args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
const server_path = args[1];
const middleware_path = args[2];
const zig_mql_out = try zig_mql.generate(
@import("src/mql5.zig"),
"mql5_trading",
);
// write all zig_mql_out.server bytes to server_path...
// write all zig_mql_out.middleware bytes to middleware...
}
And then you invoke it in your build.zig like this:
const generator_exe = b.addExecutable(.{
.name = "generator",
.root_module = b.createModule(.{
.root_source_file = b.path("generator.zig"),
.target = b.graph.host,
}),
});
const run_generator = b.addRunArtifact(generator_exe);
const generated_server_zig = run_generator.addOutputFileArg("server.zig");
const generated_middleware_zig = run_generator.addOutputFileArg("middleware.zig");
const gen_wf = b.addUpdateSourceFiles();
gen_wf.addCopyFileToSource(generated_server_zig, "generated/server.zig");
gen_wf.addCopyFileToSource(generated_middleware_zig, "generated/middleware.zig");
gen_debug_step.dependOn(&gen_wf.step);
When running a generator this way, the cache will be smart enough to only re-execute the generator once any of its inputs change. This means that you don’t even need to copy the generated files to your source directories, you could also just use them directly as root source files for regular Zig modules and it will be just as efficient in terms of build times.