I am trying to search for ways to control the 'exposure' of functions/classes/variables to third-party users while I still have full access within a C project/library.
In javascript you got modules which does this exactually. In java/C# you can get pretty far with access-modifiers. But in C/C there doesn't seem to be any control beyond the file itself (i.e. when its in the .h/.hpp file, its accessible from anywhere).
So the question, is there a way to access functions/classes/variables from files within the project without exposing them to third-party users?
CodePudding user response:
Well, don't put them on the API, and if these symbols aren't needed internally, keep them in header files only used for building your project, not installed as development headers for your consumers.
Say, you have a class declaration
class Whatpeopleuse {
private:
int _base;
public:
Whatpeopleuse(int base);
int nth_power(unsigned int exponent);
};
in your n247s/toolbox.hpp
, which you install / ship to customers.
And the implementation in your mycode.cc
file
#include "n247s/toolbox.hpp"
#include "math_functions.hpp" // contains declaration of power_function
Whatpeopleuse::Whatpeopleuse(int base) : _base(base)
{
}
int
Whatpeopleuse::nth_power(unsigned int exponent)
{
return power_function(_base, exponent)
}
with power_function
defined in another file, math_functions.cc
:
#include "math_functions.hpp"
int power_function(int base, unsigned int exponent)
{
int result = 1;
for(;exponent; --exponent)
result *= base;
return result;
}
Then you compile your mycode.cc
and your math_functions.cc
, link them together to a n247s.so
(or .dll, or whatever your shared library extension is), and ship that to the customer together with toolbox.hpp
.
Customer never sees the function definitions in math_functions.h
, or the code in math_functions.cc
or mycode.cc
. That's internal to the binary you produced.
What the customer sees/gets is
- the header
toolbox.hpp
, and what symbols / types there are in your library that they are able to access (otherwise, their compiler wouldn't know what there is to call in your library) - the binary n247s library, containing the symbols as declared in
toolbox.hpp
.
Of these symbols, only these that have visibility actually are then given a name associated with an address within the shared library file. You'll find that it's common to tell the linker that actually none of the functions in a header should be visible by default, and explicitly mark these classes and functions you want to see, using compiler __attribute__((visibility("default")))
(at least that's what's in my macros to do that, for MSVC, that attribute specification might look different).
The user of the class Whatpeopleuse
can only access its public:
members (There's ways around that, though, within limits), but they can see that you have private members (like _base
). If that's too much disclosure, you can make your customer-facing classes only contain a single member pointer, something called a detail
, and the customer-facing member functions (whose implementations just call detail->name_of_member
).
I'd like to add that you don't want to make it hard for your customers to know what your class is doing. If they're so motivated, they can reverse engineer quite a lot. Making something that's just harder to read and understand because its headers go through lengths to obfuscate what's happening behind the scenes is frustrating and people won't like it.
On the other hand, the above methodology is something you typically find in large code bases – not to "hide" things from the user, but to keep the API clean – only the things that are in the public-facing header are part of the API, the rest is never seen by the user's compiler. That's great, because it means
- you make clear what is a good idea to use, and what is internal "plumbing". This is mostly important because often, it's easy to forget what the functionality is that you actually want to offer, and then start writing confusing / hard to use libraries.
- To minimize the ABI of your library: as long as the ABI of the symbols that you have in your public-facing libraries don't change, your user can just drop-in replace your library v1.1.2 with v1.1.3, by replacing the library binary, without recompilation.
- It makes it clear what needs to be user-friendly documented, and what not. If you're shipping someone a library without documentation, they will go through hell to not use your library. I've myself have been in that position. I know companies who just rewrote whole driver suites from scratch because the documentation they got from the hardware vendor was not explaining behavior.