Building a small graphics engine, I’ve caught myself doing kind of pub fn _internal(...) from time to time to incapsulate stuff. But this cannot be the way to go in Zig, right?
How do you separate your internal implementation details from the public facing API (besides vtables) ? And do you? Since Zig doesn’t have per-field privacy, it seems very intended..
As an example, here’s one of my own package project:
The package exposes its public API from src/root.zig , while implementation details such as src/message/message_impl.zig are imported internally and are not part of the public API.
Yes. pub(crate) is actually very nice, but we obviously are not getting such feature (not complaining) and need to emulate something similar with the features we have.
But I believe this concept of handles actually solves a bit different problem. Giving out raw pointers is generally a bad idea, handles solve this. I’m talking more about actually hiding APIs which are internal-only.
Prefixing an underscore to “private” names is indeed not very idiomatic in Zig. If for some reason simply documenting that “this is an internal member and should not be used” is insufficient and you really want to go the extra mile, placing all the private implementation details with a child type is an easy soltion:
Or you could have this API file be a separate file, same thing. downside Is you write every function you want to share manualy upside is that once you actually do that it reads very very clean, because you have every declaration in one place.
If the internal functions are only needed in a single file:
(single file) you can declare them in that file without making the functions public.
If you need the function from multiple files you can:
2. declare them publicly in a file you import directly *
3. or in a whole other module B you import *
*Then don’t publicly re-exported the result of that import (or connected types) and only use the type internally which has those pub fns instead of using the type in parameters / return values / fields of your public api.
If you use 3. you can use createModule instead of addModule in the build system, that way you don’t expose your internal module to users of your package.
Or alternatively you could:
document that the function doesn’t belong to the public api (but I would prefer 1.,2. or 3. most of the time)
let people use internals if they want to, this is basically ‘don’t hide anything’ but then people need to understand what they are doing, if they choose to work with internals (could be good for very lowlevel code where it is difficult to find a good api that always works efficiently)
If you have fields you don’t want people to access you could export them only as opaque types or handles. (However opaques only hide the details, the memory is still accessible, if you can guess or find out how to interpret it (but this only matters if somebody is expected to write adversarial/malicious code))
I wonder whether it would be worth it, to add a feature to the build system, to have two lists of imports for every module one with imports that are allowed to appear in its own api and another which are only allowed internally, then the compiler would be able to give a compile error if an implementation-module-import-ed type appears in the public api (which is exported by the root file).
Might make it easier to separate api and implementation, without making a mistake and leaking implementation details.
That said, so far it seems to me that implementation details are often public in Zig, which can be a good thing, because it sometimes makes it way easier to create your own implementations which sometimes can extend existing implementations. So I would say 5. or maybe 6.‘only rarely hide something’ is also a sensible approach, because it can be annoying having to copy a whole bunch of code to be able to re-use it (or needing to fork it just for that). For example also ask yourself whether somebody could have a valid reason for wanting to wrap one of your implementations, re-using most of it directly to create a slight variant of your implementation.
zigs pub is already quite fine-grained, you can hide things by not re-exporting them:
// foo.zig
pub const x = 4;
// bar.zig
const foo = @import("foo.zig");
_ = foo.x; // can access public x
pub const x = foo.x;
// baz.zig
const bar = @import("bar.zig");
_ = bar.foo; // compile error, foo is not public.
_ = bar.x; // x is publicly re exported
const S = struct {
const z = 3;
};
_ = S.z; // works cause `S` is defined in same file