Hi @Maranix, welcome to Ziggit!
is there a reason you are trying to use a function-pointer / vtable based interface?
The problem with your interface is that it defines a function pointer that returns ParseError!void, while your implementation wants to return ParseError!usize, you need some way to be able to support the implementations different value types.
One way would be to use type erasure, for example you could change the
parseFn: *const fn (*anyopaque, []const u8) ParseError!void,
to:
parseFn: *const fn (*anyopaque, []const u8, []u8) ParseError!void,
where the third parameter would serve as the memory destination where the value is written to, on the side of the usage code you would then be able to write a function that specifies the type like this:
pub fn parse(self: *ValueParser, value: []const u8, comptime Result:type) ParseError!Result {
var res:Result = undefined;
self.parseFn(self, value, std.mem.asBytes(&res));
return res;
}
Btw your gen.parse function doesn’t work, PtrType is just *anyopaque and PtrInfo.Pointer.child which is anyopaque doesn’t have any information about what it is, you can’t use *anyopaque or anyopaque to get to a concrete implementation, because that information has been erased. (below is a changed version)
The way to use *anyopaque is to pass such a pointer to some associated function that can be called with it.
For example somewhere in your program where the type information isn’t lost yet, at that place you also create something that allows you to undo the type erasure and then use the concrete implementation to do something.
In your case that place is where the Instance of the ValueParser is created (where the int parser is packaged as a ValueParser).
I changed a few things, the parseFn has three parameters, the ValueParser.init function is called with anytype which is something very different from *anyopaque, it expects to be called with the self pointer from a concrete implementation it then uses that to create an instance of the ValueParser interface that contains a function pointer to the concrete implementations parse function.
(Think of this init function as a small helper, that converts a pointer to the concrete implementation to a fat pointer that is an instance of the interface, where the types have been erased (but the .parseFn function pointer is associated with the .ptr pointer and once it is called with it, it can restore the original type information (because it assumes that it will be called with the right self-pointer)))
The IntParser.parse implementation uses const value_ptr = std.mem.bytesAsValue(usize, dest); to turn the destination bytes into a single element pointer *usize and it then parses the string and stores the result at that address. If the given destination is too small the parse function returns error.DestinationTooSmall
const std = @import("std");
const fmt = std.fmt;
const ParseError = error{
Overflow,
InvalidCharacter,
DestinationTooSmall,
};
pub const ValueParser = struct {
ptr: *const anyopaque,
parseFn: *const fn (parser: *const anyopaque, input: []const u8, destination: []u8) ParseError!void,
const Self = @This();
pub fn init(ptr: anytype) Self {
return .{
.ptr = ptr,
.parseFn = @ptrCast(&std.meta.Child(@TypeOf(ptr)).parse),
};
}
pub fn parse(self: *const Self, value: []const u8, dest: []u8) ParseError!void {
return self.parseFn(self, value, dest);
}
};
pub const IntParser = struct {
pub fn parse(_: *const IntParser, value: []const u8, dest: []u8) ParseError!void {
if (dest.len < @sizeOf(usize)) return error.DestinationTooSmall;
const value_ptr = std.mem.bytesAsValue(usize, dest);
value_ptr.* = try fmt.parseInt(usize, value, 10);
}
pub fn parser(self: *const IntParser) ValueParser {
return ValueParser.init(self);
}
};
pub fn main() !void {
const usize_parser = IntParser{};
const parser: ValueParser = usize_parser.parser();
var usize_val: usize = undefined;
try parser.parse("123456", std.mem.asBytes(&usize_val));
std.debug.print("number: {d}\n", .{usize_val});
}
There are probably a bunch of things that could be improved about this, but it depends on what you really intend to accomplish with this, for example if the set of parser you need, is closed (meaning it is known at compile time, what parsers will be needed), it may make more sense to switch to something that is based on ducktyping and comptime code, using concrete types and generic programming, instead of runtime polymorphism (through type erased interfaces).
To figure out what solution would be best, you would have to describe in more detail what your specific requirements are, how/when you intend to use your parsers and what is known statically at compile time and what is only known at run-time.