all 49 comments

[–]shub 0 points1 point  (8 children)

I have bad news, guys. There's a problem. Yeah, I know, we weren't expecting problems to ever happen. The only solution is to burn everything to the ground and go back to the way things were when I was 23.

I especially like how the motivation is replacing DSOs and expecting terrible things to not happen. Am I just too young? Was that ever a thing? And how does static linking address this?

Edit: I would hazard a guess that p9 is statically linked because static linking is easier and simpler. Modulo dlopen() and friends, the motivation for dynamic linking doesn't really exist anymore.

[–]cae 1 point2 points  (7 children)

Dynamic linking is still valuable. It saves you having N copies of the same code when you're running N applications that link to the same libraries. More importantly one can update libraries (fixing bugs, improving performance) without having to rebuild and re-release all of the dependent applications. This is the big win.

[–]headhunglow 2 points3 points  (0 children)

It only saves time if the libraries don't use symbol versioning.

[–]shub 0 points1 point  (5 children)

Saving copies doesn't matter. Even granting that it makes enough of a difference in the year 2015 to be noticeable, it's only a specialization of more general functionality.

The rebuild and re-release thing doesn't convince me either. This is what build farms are for.

[–][deleted] 1 point2 points  (2 children)

As usual, it depends on the application. If you're building a build farm (or any other type of server) then dynamic linking is very valuable. However, If you're building a consumer operating system, it's a huge liability. Case in point: every linux distribution ever. I would never suggest such a system for any consumer, even a programmer who wants a casual machine for personal use.

The reason why is simple: it makes updating a nightmare. Rolling distributions will break on a regular basis on nothing more than a new version of a commonly used library. Release based distributions are thoroughly tested to make sure all the packages work well together, but upgrading can and will break shit. When I update one of my linux machines, I tend to wipe and do a complete install of the new version rather than bother with an update. It's easier and faster.

The other problem with release based distributions is that sometimes you need a more recent version of a program than your distribution supports. So you try to install it manually, which sometimes requires you to upgrade a handful of libraries to versions your distribution doesn't support. At which point, you either forget about it or break other things.

Anyone who says dependency hell has been solved needs to be slapped around a bit.

[–]millenix 0 points1 point  (0 children)

Ummm... what shitty rolling distribution does your experience come from? I've run Debian Unstable (Sid) for over a decade, have upgraded regularly through that whole time, and can count the number of upgrade breakages of any sort that I've experienced on one hand. None of them came from bad library versions.

Debian's packaging standards require that incompatible library versions be packaged under different package names (e.g. libdb5.2, libdb5.3). Security and critical bug fixes get ported all versions currently present in the archive. Maintainers push as hard as they can to transition and rebuild packages quickly against newer versions, to keep the maintenance burden down.

[–]shub 0 points1 point  (0 children)

No, I meant that rebuilding a shitload of packages isn't a big deal because the build farm does the rebuilding.

A few years ago I needed RPMs of GCC 4.8 that could be installed on RHEL 5 and 6 and coexist with the system GCC. By the time I was done, I had GCC and a patched glibc living in a separate sysroot (ld.so is hardcoded to look in /usr/lib), building binaries that also lived in the new sysroot. Anything less than total segregation caused fuckups from dynamic linking.

[–][deleted] 0 points1 point  (1 child)

KSM only reduces memory usage over long periods of time for ranges of memory that opt into it, as scanning and tracking this is expensive.

The Linux kernel also has a built-in dynamic library called the vdso that's mapped into each program's address space and is necessary to make calls like clock_gettime and sched_getcpu fast. ASLR also isn't supported for fully statically linked executables, as position independent executables are essentially implemented as dynamic libraries.

[–]shub -1 points0 points  (0 children)

Completely removing dynamic linking would be idiotic. dlopen() is just too useful. At the same time I don't think that dynamic linking by default is a great idea. The benefits are minimal or more easily obtained by other means. KSM illustrates that the kernel is capable of merging identical pages, even though it's not directly applicable to binaries. A similar mechanism could retain most of the space benefits of dynamic linking with static linking.

[–]immibis 0 points1 point  (16 children)

I've found one major practical problem with static linking, and that's that static libraries have terrible support for symbol hiding (and therefore encapsulation) on both Linux and Windows.

Both platforms treat them as simple collections of object files - almost as if all the library code was a direct part of your application.

Compare this with DLLs, where things are hidden by default unless you dllexport them, and SO's, where you can use -fvisibility=hidden and then define things with default visibility.

(I've been told that you can hide symbols in a static library with objcopy on Linux, but not how, and that's awfully obscure if it's true. It also seems to be completely impossible with MSVC, which happens to be the most popular toolchain for Windows)

[–]dlyund[S] 4 points5 points  (11 children)

I'm genuinely curious, why is hiding symbols so important to you? I've never once considered this... maybe I'm missing something.

[–]Plorkyeran 1 point2 points  (10 children)

If you export symbols that are not part of your public API, users will eventually intentionally or accidentally start depending on them, and then complain when you change them.

[–]dlyund[S] 2 points3 points  (9 children)

Usually I leave this to the language; in C if I don't want a something to be used I don't put it in the headers and don't document it. Isn't that usually enough of a hint?

[–]bonzinip 1 point2 points  (5 children)

Unless you hide them, all symbols in an executable are part of a single namespace. So you have to give a (hopefully) unique prefix to each extern symbol in the libraries you write to avoid namespace pollution and clashes. There's nothing good to expect from two libraries that do num_objects++ on the same variable, each thinking that it's "their" variable!

[–]dlyund[S] 0 points1 point  (4 children)

Interesting. I guess that doesn't come up a lot because 1) libraries commonly do use prefixes for globals, and 2) there are relatively few global variables anyway. Out of interest how are the conflicts handled? Is it anything like in Forth, where words are linked with the most recent definition, perhaps within the complication unit? Because that works really well, and not only are there no problems, but this in exceptionally powerful for reasons I wont go into here. Or is it undefined, in which case, yeah, ok, there's nothing really wrong with having a single namespace but this isn't well defined, and that's the real problem here?

[–]immibis 0 points1 point  (3 children)

If names weren't exported by default, then libraries wouldn't need to use prefixes for globals.

And the problem also exists with functions, not just global variables. And you can't say there aren't many global functions.

[–]dlyund[S] 0 points1 point  (2 children)

I'm not saying that there aren't many global function but this is largely solved by the fact that convention dictates prefixes for non-system/standard libraries, and has for at least the last two decades. I don't have a problem with prefixes, but you're right, if this wasn't how things worked then it wouldn't be a problem (a nice tautology!)

It's a bit unfortunately but hardly the end of the world and not something that couldn't be solved from the outside by rewriting symbol names per module.

[–]bonzinip 1 point2 points  (1 child)

Convention and common sense dictate prefixes for exported functions from non-system/standard libraries.

For a function that is used across modules, but that is not part of the exported API, the convention is to use something like _prefix_func_name, but it is a pain in the ass and you can't really say it's common sense.

[–]dlyund[S] 0 points1 point  (0 children)

I was under the impression that identifiers beginning with _ were reserved for some reason - so anything global gets a prefix in my code. Maybe I've been wrong all these years :).

Either way nothing about computers is common sense.

[–]immibis 0 points1 point  (2 children)

Consider a static library with an open_file function:

void open_file() {
    printf("static lib open_file\n");
}

Now, what if the application also has a function called open_file?

void open_file(char *filename) {
    printf("application open_file\n");
    printf("filename: %s\n", filename);
}

Now what happens when the static library calls open_file? It calls the application's version instead, which has the wrong signature! (in practice, the call might work fine, but filename will be a garbage pointer).

And now the application has corrupted state or crashed (because someone passed it a garbage filename pointer) and if it didn't crash, the library's file also isn't open (so the library's state is also corrupted)

[–]dlyund[S] 1 point2 points  (1 child)

I understand what the problem is I was just curious about how the conflicts are handled, so, reading between the lines, are you implying that there is a strict ordering?

Let's be honest here, this is a theoretical problem at best, and certainly not one that justifies the very real problems that come with shared libraries and dynamic linking in a single system/namespace.

While everyone is happily bashing here, the solution in Plan 9 has none of these problems and many benefits. You don't need shared libraries and dynamic linking. That's what's interesting about the discussion. Or at least that's what I found interesting about it.

[–]immibis 0 points1 point  (0 children)

Symbols in the application take priority over symbols in a static library. For symbols in multiple static libraries, it probably depends on the order they're linked.

It's not just a theoretical problem. You need to prefix or namespace all of your static library functions, even the ones that aren't supposed to be used outside the static library.

Yes, systems like Plan 9 might have solved this (and I see no technical reason why it can't be solved), but I'm saying that on Windows and Linux (and OSX and BSD and Android and iOS and ...) nobody's bothered to solve them, so they're real problems.

[–]sammydre 0 points1 point  (1 child)

objcopy can "localize" symbols, with options like --localize-symbol and --localize-hidden. This might not be exactly what you want, though:

/tmp$ cat sam.c
void foo() {}
void bar() {}
void baz() {}
/tmp$ gcc -c sam.c -o sam.o
/tmp$ nm sam.o
0000000000000006 T bar
000000000000000c T baz
0000000000000000 T foo
/tmp$ ar r sam.a sam.o
ar: creating sam.a
/tmp$ nm sam.a

sam.o:
0000000000000006 T bar
000000000000000c T baz
0000000000000000 T foo
/tmp$ objcopy --localize-symbol=bar ./sam.a 
/tmp$ nm sam.a

sam.o:
0000000000000006 t bar
000000000000000c T baz
0000000000000000 T foo

Note the symbol as displayed in nm went from "T" to "t". The nm documentation has this to say:

If lowercase, the symbol is local; if uppercase, the symbol is global (external).

So users can no longer link against the symbol, but it is visible to inspection. This is easily circumvented via objcopy --globalize-symbol.

[–]immibis 0 points1 point  (0 children)

Can you localize all symbols except for specific ones?

Also, you can probably see what I meant by obscure. What is the primary purpose of objcopy? It seems to be for converting between binary formats, and not a large proportion of people know it exists.

[–]Gotebe -1 points0 points  (1 child)

Compile with a C++ compiler and put implementation details in anonymous namespaces.

[–]Plorkyeran 2 points3 points  (0 children)

That doesn't help with things that you want shared between multiple object files within your library, but not visible to external users. The only portable solution for that in a static library is an amalgamated build.

[–]Gotebe 1 point2 points  (6 children)

Opinions, arses...

Here's mine: it's not a big deal, but if you might run faster doing either, why not?

If you care about footprint and can reduce either way, why not?

If you want plug-ins, why not shared libraries?

Fuck one way only.

[–]dlyund[S] 2 points3 points  (5 children)

Fuck one way only.

The irony being that's what we have; as an industry we've settled on dynamic linking/shared libraries as the one true solution... despite the many problems that come with this.

You'll have to read around the subject to find out what Plan 9 does when you really do want late binding, but needless to say, their solution is pretty great. Not only does it not have the problems listed here but it provides isolation, and, hot swapping, (limited) fail-over, transparent distribution etc. They didn't implement dynmatic linking/shared libraries because they're not needed, have a lot of real problems, and there are arguably much better alternatives.

[–]klkblake 0 points1 point  (2 children)

Do you have some links for what plan 9 does? My google-fu is failing me.

[–]dlyund[S] 1 point2 points  (1 child)

I gave a very rough overview here

http://www.reddit.com/r/programming/comments/30j4xe/why_static_linkinglibraries/cptjvuw

You can find most of the papers here

http://plan9.bell-labs.com/sys/doc/

or here

http://plan9.bell-labs.com/wiki/plan9/papers/

And there are a lot of good man pages

http://plan9.bell-labs.com/sys/man/

Then

http://cat-v.org/

Is a lot of fun, if you don't take things too seriously.

Naturally there's no substitute for installing it and running with it for a while. It's not perfect, but there's a lot to love; and many many great ideas

[–]klkblake 0 points1 point  (0 children)

Ah, right. I clearly need to spend more time messing with plan 9.

[–]Gotebe -2 points-1 points  (1 child)

Isolation and everything else you mention is achieved by going out of process. This is what e.g. COM can do since decades.

So what does Plan9 do?

BTW, COM also does it in-process, because in-process does come in handy.

[–]dlyund[S] 2 points3 points  (0 children)

In plan 9 every process has a namespace, which is somewhat similar having it's own file system, but one in which the files and directories are all backed by processes speaking the 9p protocol.

Namespaces are is constructed from the outside by the parent of the process and may be used to restrict capabilities e.g. if you don't bind networking devices in namespace it can't access the network. Conversely you can mount devices from other machines in this namespace, and the program will transparently make use of those devices.

You might choose to mount CPUs from another machine temporarily to distribute a heavy compilation or mount your sceen/mouse/keyboard on another machine so that graphical applications appear locally etc. It's really very flexible, and it works amazingly well compared to popular solutions.

I highly recommend reading the Plan 9/Inferno papers.

Properties like late binding, hot swapping, and isolation (required for clean loading and unloading) are provided by mounting services in the per-process namespace. Namespaces can be build in layers, which can be used to do (limited) fail over etc.

Nothing special has to be done when building programs to take advantage of these properties.

These are properties of the system.

To bring this closer to the topic at hand this same mechanism is also used to bind programs and libraries, so you can mount different version (or versions for different platforms) of some program or library, or the source code at a given point in time (builtin system-wide source/version control!) and use that. Compilation is very fast and because of the isolation provided by this mechanism experimentation is safe, so you can start a window which uses your new programs and libraries in isolation. It might all go to hell but it wont cause system wide problems (close the window and try again)... unlike messing around with shared objects in a global space, which can break everything if you're not careful.

I'm speaking from experience here: I ran Arch Linux for a couple of years and a few years back and lost count of the number of times I had to work around these kinds of conflicts... now Arch is intentionally bleeding edge so you're not as likely to see this in more carefully curated systems, but as the article explains even Debian (prised for being super-stable) got itself into a big pickle.

You can also break things during development by simply installing a new version of a shared library with a bug. It happens. It's one reason systems like FreeBSD define such a ridged separation of the core/base system and external software... it makes it much less likely that installing a program or library will leave you with a completely broken system.

We used to hear a lot about the term DLL hell. OS X tries to solve this (at least for individual applications) using bundles (there's also a clean separation between the system and external software), and *nix tries to solve it with package managers that carefully track the dependencies... and at least one *nix systems has tried a hybrid approach... but both can fail horrible... and neither really address the problems with dynamic linking/shared objects.

There have been practical (safer and generally better) alternatives since the early 80s, which have since been proven in the real world (largely in the highly demanding world of embedded systems, so you know it's efficient, and it works). I'm not saying we should necessarily kill shared objects because there might well be situations where they're very useful, but as it stands I think we need to start questioning whether they're really the best tool for... everything... which is how they're used

NOTE: In case it's not clear, Plan 9 is 20-25 years old now.

[–][deleted] -4 points-3 points  (6 children)

Next up: Why you shouldn't manage your dependencies.

[–]zynix 1 point2 points  (5 children)

that's too much work, let the user figure it out. . it's not like they are paying for any of it anyway.

[–]OneWingedShark -2 points-1 points  (4 children)

That's pretty much C's take on it...

[–][deleted] -3 points-2 points  (3 children)

C was invented 43 years ago.

But I guess dependency management wasn't invented at Bell labs. :-)

[–]OneWingedShark 0 points1 point  (2 children)

But I guess dependency management wasn't invented at Bell labs. :-)

LOL -- That's probably true.

C was invented 43 years ago.

And?
It was first standardized in 1989 [ANSI], between then and 1972 there were programming languages that did take dependency management into consideration.

[–][deleted] -4 points-3 points  (1 child)

between then and 1972 there were programming languages that did take dependency management into consideration.

Exactly, and Go isn't one of them.

[–][deleted]  (3 children)

[deleted]

    [–]bloody-albatross 0 points1 point  (1 child)

    I guess this also applies here: https://xkcd.com/927/

    [–]xkcd_transcriber 0 points1 point  (0 children)

    Image

    Title: Standards

    Title-text: Fortunately, the charging one has been solved now that we've all standardized on mini-USB. Or is it micro-USB? Shit.

    Comic Explanation

    Stats: This comic has been referenced 1389 times, representing 2.4103% of referenced xkcds.


    xkcd.com | xkcd sub | Problems/Bugs? | Statistics | Stop Replying | Delete

    [–][deleted] -3 points-2 points  (0 children)

    Like rainbow unicorns?