• entries
    58
  • comments
    218
  • views
    114346

Friend Assemblies

Sign in to follow this  

1165 views

In lieu of the tutorial that I've been stalling on for a while now (Evil Steve's magnificent D3D9 tutorial has me depressed), I'll be discussing friend assemblies, and how something that works quite well with C# got absolutely and totally screwed up by C++/CLI.

OK, so, friend assemblies. What are they? Well, suppose you have a library that has a few public types that consumers of your library use, and few internal helpers that assist the others in performing the library's tasks. This is great, and how encapsulation is meant to work. Hide things from the end user that he doesn't need to see, both so that things are simpler and so that he won't inadvertently break something. What happens, though, if you want to write a another library that extends the functionality of the first library, but in a completely separate DLL? You run into problems if you need to use the internal utilities provided by library A. This is where friend assemblies come into play.

The CLI allows library designers to mark assemblies with an attribute called InternalsVisibleTo, which allows them to specify a list of assemblies that can see and use the internals of the marked assembly. This is straightforward, and is also tightly controlled, since the only person who can specify other friend assemblies is the original library developer. It's probably not ideal Object Oriented Programming, but that doesn't really bother me. Now, in C#, this functionality works great and as expected. Library A marks itself with this attribute, and lets the compiler know that library B can touch its private parts. All is right with the world.

Enter C++/CLI, stage left. When it came time to implement friend assemblies for C++/CLI, the language design team had a collective brain malfunction, because things get very crazy, very fast. Marking library A with InternalsVisibleTo still follows the same procedure as outlined for C#. The problem comes with the consumer assembly, which is library B in our case. It isn't enough for the C++/CLI compiler to see that A has declared B a friend. It wants more proof that the two assemblies really want to be friends. OK, I can live with that; maybe there's some strange goings-on under the hood in the C++/CLI compiler that require this. However, somebody on the design team thought it would be hilarious to require the reference to assembly A to be made using a #using statement, with a magical as_friend modifier stuck on the end. Apparently, the syntax team didn't get the memo, as this magical keyword isn't identified as such by Visual Studio.

OK I say to myself. It's weird, but I can deal with this. Just stick "#using "AssemblyA" as_friend" at the top of a source file and everything should work. Right? Wrong. When the compiler hits the #using statement, it says to itself (and me as well, how nice of it) "hmm, you've already added AssemblyA as a reference using the project settings, and it isn't marked as_friend there, so I'm just going to ignore the fact that you said as_friend here. I'm so helpful!" Of course, there's no way to mark an assembly as_friend from the Add Reference dialog, so you have to remove the assembly from the list and stick with the #using statement.

But wait, there's more. The #using statement, confusingly, only applies to the source file in which you've declared it. That's right. Even though your assembly B has told the compiler that you're using a reference to assembly A, it will still only let you use types from assembly A if you've made the special #using statement in the current source file. And since we've removed assembly A from the project-wide list of references, you now need to make sure that every required source file can see the #using statement.

But wait, there's more! The #using statement can only refer to ONE specific DLL, with a hard coded path. That's right. You need to hard code the path to the DLL. That means that if you want to refer to an assembly contained in the same solution, you need to refer to its actual physical location on disk. Also, you end up needing to use preprocessor magic to make sure you load the right assembly based up on the current platform and configuration of the project. In SlimDX, this preprocessor garbage ends up looking like this:


#ifdef X64
#ifdef PUBLIC
#using "../build/x64/Public/SlimDX.dll" as_friend
#else
#ifdef NDEBUG
#using "../build/x64/Release/SlimDX.dll" as_friend
#else
#using "../build/x64/Debug/SlimDX.dll" as_friend
#endif
#endif
#else
#ifdef PUBLIC
#using "../build/x86/Public/SlimDX.dll" as_friend
#else
#ifdef NDEBUG
#using "../build/x86/Release/SlimDX.dll" as_friend
#else
#using "../build/x86/Debug/SlimDX.dll" as_friend
#endif
#endif
#endif



So now you need to add this to a common header and make sure you include it from every source file, making compile times that much worse and leading to weird and obscure compile errors if you forget to add it to one. I cannot fathom what they thought they were doing when they designed this system, but I do know that it's sick and twisted. Wherever you are, C++/CLI designer, know that I shake my fist at your evil shenanigans.
Sign in to follow this  


1 Comment


Recommended Comments

Hmm. Perhaps they wanted such a non-standard form to deviate from standard form as much as inhumanly possible. Putting a couple underscores in front of it couldn't possibly have worked like it always did in the past. Does this also apply to partial classes, i.e., if part of a class is in a separate file that isn't marked as_friend, will the members of that class be invisible to external modules? Or does C++ not support partial classes? I'm VB/C# myself.

Share this comment


Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now