36

I was reading Pouzin's comments on the early Multics shell, and I'm a bit confused about this passage:

In the same vein, I also felt that commands should be usable as library subroutines, or vice versa.

Given changes in terminology over the years, this could mean practically anything, but this was posted in 2000 so I assume "library subroutines" means what it does today.

So what exactly was going on here? Were library entry points published and callable directly from the shell?

2
  • 4
    That's my understanding, though an entry point to be used as a command may have been restricted in argument types (e.g., strings only). This might have more detail
    – dave
    Commented Dec 31, 2022 at 15:09
  • Further - as I understand it, typing 'foo bar mumble' causes segment 'foo' to be made known to the process, and linkage to entry 'foo$foo' to be set up; then a call to foo$foo("bar", "mumble") is executed. Given that, the structural distinction between command and subroutine vanishes - except that a command might engage in user dialogue on its terminal, whereas a subroutine has output arguments, so the implementation of the command/subroutine would have to know its intended use.
    – dave
    Commented Dec 31, 2022 at 17:36

4 Answers 4

29

Yes. All external entry points in compiled "segments" ( Multics for "files") were exposed as names in the file system. Very strange by today's standards but that's what they did. And thus that entry point was able to be named by the user and called by the command processor, with no extra step needed (i.e., no "wrapper", or declaration to the command processor, or anything else). (It had to have the right "signature" - all parameters had to be strings.)

Even aside from that use, there was no "link step" needed for any program. You compiled your code, then added all the external entry point names as additional names for that segment. (Perhaps it was done automatically by the compiler, I don't remember.) Anyway, the "linker" was built into the operating system and would just look for the right name in the file system and link to that. What we would call, today, "dynamic linking" except even more dynamic since it applied to individual entry points, not entire DLLs/SOs. (In fact, it is what Windows calls "delayed loading" - the search for the external entry point was not attempted until it was actually called. And this was done automatically by the system, on an entry point by entry point basis; it wasn't like "run-time" dynamic linking in Windows which is where the programmer explicitly calls LoadLibrary and GetProcAddress himself.)

I reference this mechanism in this answer. And see also this answer of John Doty's w.r.t. "DLLs".

6
  • 1
    Good post, thanks for making it. I cleaned up the other chatter. Commented Jan 1, 2023 at 18:43
  • 1
    You could have just skipped the arrow entirely, it’s just as legible… Commented Jan 1, 2023 at 22:00
  • Excellent post. It would seem from the CLI user's perspective there is little difference between having a name for a function and the Unix solution of having small separate programs and stringing them together with pipes, launch-time considerations aside. However, the idea of being able to use the exact same code in PL/1 and the shell... well that really does seem very interesting indeed. It would be invaluable in debugging! Commented Jan 2, 2023 at 19:14
  • 2
    Thinking on it further, I'm actually surprised this didn't re-appear in Plan9, given it was trying to really have "everything is a file". But then when you look, that ultimately means they extended it to /proc and a couple of other things, whereas this concept, of "multiple main" as it were, really does make everything a file. Commented Jan 2, 2023 at 19:20
  • 1
    @davidbak No, ACL was per-segment, not per-name. Commented Jan 4, 2023 at 21:48
19

Commands in Multics were functions, not standalone programs. So, the command:

delete myfile

was the same as:

call delete( "myfile" );

in PL/I.

In practice, this was rather troublesome. Command functions were restricted to taking arguments of character string type, so most functions could not be used as commands. Functions designed to be commands generally used different error handling (print a message versus returning a code) than functions designed to be called by a program. However, commands and other functions shared the same name space. It was easy to do something crazy by inadvertently invoking a function not designed as a command. Thus, there was a convention that most non-command functions had names ending in "_". For example, the Multics equivalent of printf() was ioa_(). But this didn't work for things like imported libraries, whose functions had more usual names.

4
  • 1
    Thanks for the details about how this actually worked!
    – davidbak
    Commented Jan 1, 2023 at 18:31
  • And here's a great previous answer of yours that also explains this (elaborated in the comments).
    – davidbak
    Commented Jan 1, 2023 at 18:49
  • It seems this could have been implemented in Unix by having main be a compiler default but allowing zero or more functions also act as additional entry points. One naturally makes main handle argv, so it is not too much of a stretch to ask programmers to do the same in their other entry points, both calling the actual internal work methods that have arbitrary parameters. Is that basically the analogue? Commented Jan 2, 2023 at 19:24
  • @MauryMarkowitz main() isn't called by the shell: it's called by crt0. Nothing calls crt0: it starts execution in a brand-new process image. Meanwhile, after fork(), the parent process continues to execute. This is not normal subroutine linkage.
    – John Doty
    Commented Jan 3, 2023 at 1:34
7

If you want an analogy with a modern system, when you executed a command

command_name arg1 arg2 arg3 ...

The shell (called "command processor" on Multics) would perform the rough equivalent of this on Unix:

  • Parse the arguments into an array of strings.
  • find the command_name file using the search path.
  • Open it using dlopen()
  • Get a pointer to the function using dlsym("command_name")
  • Call the function with the above argument array.

So it's not too different from the way Unix execvp() works. The significant differences are that it's not restricted to calling the main() function, and there's no fork() before loading and executing the program (on Multics, new processes were generally only created when logging in -- the entire login session is one big process with programs executed as subroutines).

Notice that this can only be used to pass string arguments. There's a command (whose name escapes me) that could be used to call arbitrary procedures. In the argument list you used options to specify the datatype of each argument, something like:

proc_call name -fixed_dec 123 -char abc -output fix_dec

This would parse the arguments, then use a process similar to the above to call the function with that argument list.

6
  • 2
    Good explanation, except execvp() only returns if it can't execute the program, so I think mentioning it can only confuse the reader.
    – John Doty
    Commented Jan 2, 2023 at 13:54
  • Ok, got it. One follow-up, how did the system "display" these as separate tasks? If I invoked A.B("hello, world") and then A.B("goodbye, world"), I understand how Unix handles this as they are completely separate tasks. But if these tasks have long lifetimes, is it up to the programmer to ensure the state is properly handled? Does reentrancy come into play as well? Commented Jan 2, 2023 at 19:18
  • 1
    They're just separate function calls. When the first one returns, the second one executes.
    – Barmar
    Commented Jan 2, 2023 at 19:20
  • 1
    Multics doesn't have per-process multi-tasking in the OS. Towards the end of its lifetime a userspace multi-threading library was implemented.
    – Barmar
    Commented Jan 2, 2023 at 19:23
  • 1
    I'm tempted to make an analogy with RUNDLL32.EXE :)
    – hobbs
    Commented Jan 3, 2023 at 15:55
3

Presuming you mean this article, it seems clear from his habit of writing CTSS commands in a a high-level language ("MAD", the Michigan Algorithm Decoder) that he means something similar to the code that implements shell commands existing in a subroutine library.

That is not a surprising concept in itself, although there's a missing step: the software that takes strings typed at a terminal and turns them into calls to that subroutine library.

As things have turned out, modern shells have a lot of built-in commands, and usually invoke "external" commands from individual program executables, rather than libraries. Those executables use shared libraries, but there isn't usually a direct relationship between shared library functions and command-line commands.

5
  • 2
    This doesn't ring true to me. It implies, for example, that if I write a new command 'foo' then someone needs to augment the shell's library to add a wrapper function (which we might refer to as 'f$foo') before it can be used. Furthermore, there's the difficulty of getting the output of the command foo as the return value of f$foo. On VMS, the lexical functions essentially wrap system calls (and CLI routines), not commands.
    – dave
    Commented Dec 31, 2022 at 17:31
  • @another-dave: You're right: Removed the DCL example, added a different one. Commented Dec 31, 2022 at 18:58
  • There still isn't that direct relationship with Toybox/Busybox, for some definitions of "direct". Toybox/Busybox look at the first command line argument and do an internal call to the right routine. In Multics, when the command processor calls out to a program the system linker finds the right entry point in the file system and links it into the process right then and there and an actual machine-instruction call is made, just as if it had all been compiled together. That command becomes "part" of the shell (in the modern view, of shared libraries/DLLs).
    – davidbak
    Commented Dec 31, 2022 at 19:06
  • Busybox doesn't ring true either - that's basically one program that internally implements a lot of commands as subroutines. It does not change the fact than an external-to-busybox command cannot be called as a subroutine, and nor does it allow other prorgams to call busybox routines.
    – dave
    Commented Dec 31, 2022 at 22:28
  • @davidbak: Removed Toybox. Commented Dec 31, 2022 at 23:37

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .