I have a Path struct and PathFn that I’m using in CopyStrategy and LinkStrategy structs:
// --- PATH STRATEGY ---
/// CopyStrategy and LinkStrategy contexts.
const PathFn = *const fn (ctx: *anyopaque, src: []const u8, anchor: []const u8) anyerror![]const u8;
const PathResolvers = struct {
/// !flatten + !mkdir
fn resolveNone(ctx: anytype, src: []const u8, _: []const u8) ![]const u8 {
return try Io.Dir.path.join(ctx.gpa, &.{ ctx.dest, src });
}
/// flatten + !mkdir
fn resolveFlatten(ctx: anytype, src: []const u8, anchor: []const u8) ![]const u8 {
const stripped = stripAfterAnchor(src, anchor);
return try Io.Dir.path.join(ctx.gpa, &.{ ctx.dest, stripped });
}
/// !flatten + mkdir and flatten + mkdir
fn resolveFlattenMkdir(ctx: anytype, src: []const u8, anchor: []const u8) ![]const u8 {
const stripped = stripAfterAnchor(src, anchor);
return try Io.Dir.path.join(ctx.gpa, &.{ ctx.dest, anchor, stripped });
}
};
CopyStrategy and LinkStrategy:
// --- File STRATEGY ---
const CpLogicFn = *const fn (ctx: *const CopyStrategy, src: []const u8, dst: []const u8) anyerror!void;
const CopyStrategy = struct {
io: Io,
gpa: Allocator,
buf: []u8,
acl_list: []u8,
acl_value: []u8,
dest: []const u8,
max_chunk: usize,
falloc_switch: *bool,
algo: ChecksumAlgo,
path_resolver: PathFn,
copy_logic: *const fn ( ctx: *const CopyStrategy, src: []const u8, dst: []const u8,) anyerror!void,
....
// --- LINK STRATEGY ---
const LnLogicFn = *const fn (ctx: *const LinkStrategy, src: []const u8, dst: []const u8) anyerror!void;
const LinkStrategy = struct {
io: Io,
gpa: Allocator,
buf: []u8,
acl_list: []u8,
acl_value: []u8,
dest: []const u8,
// Function pointers resolved once at build()
path_resolver: PathFn,
link_logic: *const fn (ctx: *const LinkStrategy, src: []const u8, dst: []const u8) anyerror!void,
....
All of that resides in CopyEngine struct where I have lookup tables, wrapper and a build function:
/// Fixed signature for all path resolvers
fn wrapPath(comptime T: type, comptime func: anytype) PathFn {
return struct {
fn wrapper(ptr: *anyopaque, src: []const u8, anchor: []const u8) anyerror![]const u8 {
// Recover the original pointer type
const self: *const T = @ptrCast(@alignCast(ptr));
// Call the generic resolver
return try func(self, src, anchor);
}
}.wrapper;
}
/// Wrapper for path strategy used in file copy strategy
const cp_path_res_lookup = [4]PathFn{
wrapPath(CopyStrategy, PathResolvers.resolveNone), // 00
wrapPath(CopyStrategy, PathResolvers.resolveFlattenMkdir), // 01 (Consolidated)
wrapPath(CopyStrategy, PathResolvers.resolveFlatten), // 10
wrapPath(CopyStrategy, PathResolvers.resolveFlattenMkdir), // 11
};
/// Wrapper for path strategy used in link copy strategy
const ln_path_res_lookup = [4]PathFn{
wrapPath(LinkStrategy, PathResolvers.resolveNone), // 00
wrapPath(LinkStrategy, PathResolvers.resolveFlattenMkdir), // 01 (Consolidated)
wrapPath(LinkStrategy, PathResolvers.resolveFlatten), // 10
wrapPath(LinkStrategy, PathResolvers.resolveFlattenMkdir), // 11
};
/// The master build function that sets up and resolves all runtime branching.
pub fn build(
io: Io,
gpa: Allocator,
buffers: *Buffers,
buf: []u8,
dest: []const u8,
max_chunk: usize,
acls: bool,
checksum: bool,
algo: ChecksumAlgo,
mkdir: bool,
flatten: bool,
falloc_switch: *bool,
) CopyEngine {
// Bitwise Indexing (flatten: bit 2, mkdir: bit 1, acls: bit 0)
// 0b000 - 0b111
const idx: u3 = (@as(u3, @intFromBool(flatten)) << 2) |
(@as(u3, @intFromBool(mkdir)) << 1) |
(@as(u3, @intFromBool(acls)));
// ACL Strategy
const acl_ctx: ?AclContext = if (acls) .{
.io = io,
.gpa = gpa,
.dest_root = dest,
.acl_list = buffers.acl_list.allocatedSlice(),
.acl_value = buffers.acl_value.allocatedSlice(),
// Setup of the ACL execution function
.exec = acl_exec_lookup[idx],
} else null;
// Copy strategy indexing
// (acls: bit 1, checksum: bit 0)
const cp_idx: u2 = (@as(u2, @intFromBool(acls)) << 1) | @as(u2, @intFromBool(checksum));
// Path resolver strategy indexing (2 bits: flatten, mkdir)
// 00: None, 01: Mkdir, 10: Flatten, 11: FlattenMkdir
const res_idx: u2 = (@as(u2, @intFromBool(flatten)) << 1) | @as(u2, @intFromBool(mkdir));
return .{
.gpa = gpa,
.buffers = buffers,
.mkpath_ctx = .{
.io = io,
.gpa = gpa,
.dest = dest,
.acl_ctx = acl_ctx,
// Setup of the path creation strategy
.strat = mk_lookup[idx],
},
.copy_strategy = .{
.io = io,
.gpa = gpa,
.buf = buf,
.acl_list = buffers.acl_list.allocatedSlice(),
.acl_value = buffers.acl_value.allocatedSlice(),
.dest = dest,
.max_chunk = max_chunk,
.falloc_switch = falloc_switch,
.algo = algo,
// Setup of the path strategy
.path_resolver = cp_path_res_lookup[res_idx],
// Setup of the file copy strategy
.copy_logic = cp_lookup[cp_idx],
},
.link_strategy = .{
.io = io,
.gpa = gpa,
.buf = buf,
.acl_list = buffers.acl_list.allocatedSlice(),
.acl_value = buffers.acl_value.allocatedSlice(),
.dest = dest,
// Setup of the path strategy
.path_resolver = ln_path_res_lookup[res_idx],
// Setup of the link copy strategy
.link_logic = if (acls)
@as(LnLogicFn, LinkStrategy.linkAcl)
else
@as(LnLogicFn, LinkStrategy.link),
},
.arg_path_strategy = .{
.io = io,
.gpa = gpa,
.dest = dest,
// Setup of the arg path destination strategy
.strat = arg_path_lookup[res_idx],
},
.file_op = if (flatten) copyFlat else copyStandard,
.link_op = if (flatten) linkFlat else linkStandard,
};
}
Functions where the resolver is used:
/// Private function called to process files in a directory tree.
/// Call from CopyEngine instance.
fn recCp(
ctx: *const CopyStrategy,
files: *std.ArrayList([]const u8),
start_files: usize,
anchor: []const u8,
) !void {
while (files.items.len > start_files) {
const src_path = files.pop();
defer ctx.gpa.free(src_path.?);
std.debug.assert(src_path != null);
// Zero branching for flatten/mkdir/acls here:
const dest_path = try ctx.path_resolver(@ptrCast(@constCast(ctx)), src_path.?, anchor);
defer ctx.gpa.free(dest_path);
std.log.debug("Copying: {s} to {s}", .{ src_path.?, dest_path });
ctx.copy_logic(ctx, src_path.?, dest_path) catch |err| {
std.log.err("Failed to copy: {s}\n{s}\n", .{
src_path.?,
@errorName(err),
});
};
}
}
/// Private function called to process links in a directory tree.
/// Call from CopyEngine instance.
fn recLn(ctx: *const LinkStrategy, sym_links: *std.ArrayList([]const u8), start_links: usize, anchor: []const u8) !void {
while (sym_links.items.len > start_links) {
const entry = sym_links.pop();
defer ctx.gpa.free(entry.?);
std.debug.assert(entry != null);
// Zero branching here
const sub_path = try ctx.path_resolver(@ptrCast(@constCast(ctx)), entry.?, anchor);
defer ctx.gpa.free(sub_path);
std.log.debug("Copying {s} to {s}", .{ entry.?, sub_path });
// Direct jump to pre-selected logic (Acl or NoAcl)
ctx.link_logic(ctx, entry.?, sub_path) catch |err| {
std.log.err("Failed to copy: {s}\n{s}\n", .{
entry.?,
@errorName(err),
});
};
}
}
Is there a better solution than using a wrapper for pointer conversion?
My initial goal was to process all the runtime flags once during a setup, and everywhere else to have a branchless code in loops.