Why does fopen take a string as its second argument?
Asked Answered
H

9

19

It has always struck me as strange that the C function fopen() takes a const char * as the second argument. I would think it would be easier to both read your code and implement the library if there were bit masks defined in stdio.h, like IO_READ and such, so you could do things like:

FILE *myFile = fopen("file.txt", IO_READ | IO_WRITE);

Is there a programmatic reason for the way it actually is, or is it just historic? (i.e. 'That's just the way it is.')

Hisakohisbe answered 25/3, 2010 at 20:50 Comment(1)
I have always been bothered by this in the C libraryFarceur
B
8

One word: legacy. Unfortunately we have to live with it.

Just speculation: Maybe at the time a const char * seemed more flexible solution, because it is not limited in any way. A bit mask could only have 32 different values. Looks like a YAGNI to me now.

More speculation: Dudes were lazy and writing "rb" requires less typing than MASK_THIS | MASK_THAT :)

Bridgetbridgetown answered 25/3, 2010 at 20:58 Comment(12)
Why though? Masks seem the natural choice here, especially in the day fopen was designed.Ramey
@GMan: As I speculated in my answer, I'm wondering if programmers' time was seen as the more valuable resource. But I have no real proof.Penitence
@GMan: I'm not sure masks are necessarily a more natural choice than a string descriptor. Either are reasonable, defensible design options.Alterable
"unfortunately"? how often does it really annoy you?Markham
I'm going to check this answer, simply because it seems like the most likely explanation. I like Michael Burr's answer as well, but as he said, this is much less likely to have crossed Mike Lesk's mind I imagine. In retrospect, although this question does probably have one definitive answer, I can't really verify anything to make a correct selection! My mistake.Hisakohisbe
"A bit mask could only have 32 different values" -- When C was invented, a bit mask could only have 16 different values.Lobworm
@Windows programmer: No, C does not have a bit mask type. Any integral type will work (preferably unsigned), and unsigned long is a minimum of 32 bits.Belfry
When C was invented, the second parameter to open() had type int, int had 16 bits, unsigned didn't exist, and long didn't exist. The second parameter to open() was an int that was used as a bit mask. Since the int was being used as a bit mask, 16 bits could represent 16 different values.Lobworm
And note that back in the era when the standard I/O library was defined and described (Version 7 Unix and earlier), the open() system call only took two arguments. If open() failed because the file didn't exist, you had to call creat() with the name and mode to create the file.Bebel
Note too that const was not a keyword in C for more than a decade after the (pre-standard) Standard I/O library was written. When the C standard was written, it became possible to state that the C library functions would not modify the strings passed as arguments.Bebel
But the number of possible open flags can easily fit in even 16 bitsFarceur
@user16217248 But the number of possible open flags can easily fit in even 16 bits No they can not. There are 30 different options just for recfm there.Kimmie
B
12

I believe that one of the advantages of the character string instead of a simple bit-mask is that it allows for platform-specific extensions which are not bit-settings. Purely hypothetically:

FILE *fp = fopen("/dev/something-weird", "r+,bs=4096");

For this gizmo, the open() call needs to be told the block size, and different calls can use radically different sizes, etc. Granted, I/O has been organized pretty well now (such was not the case originally — devices were enormously diverse and the access mechanisms far from unified), so it seldom seems to be necessary. But the string-valued open mode argument allows for that extensibility far better.

On IBM's mainframe MVS o/s, the fopen() function does indeed take extra arguments along the general lines described here — as noted by Andrew Henle (thank you!). The manual page includes the example call (slightly reformatted):

FILE *fp = fopen("myfile2.dat", "rb+, lrecl=80, blksize=240, recfm=fb, type=record"); 

The underlying open() has to be augmented by the ioctl() (I/O control) call or fcntl() (file control) or functions hiding them to achieve similar effects.

Bebel answered 26/3, 2010 at 14:19 Comment(7)
Thanks Jonathan. That's another interesting advantage of strings I was not aware of.Hisakohisbe
See IBM's MVS fopen() documentation for actual examples of this.Kimmie
on Windows it can receive the encoding: fopen("newfile.txt", "rt+, ccs=encoding")Shrewmouse
Instead fopen could have been a variadic function that takes binary flags and optional implementation defined extra arguments.Farceur
@user16217248 Instead fopen could have been a variadic function ... Not with the current API. Unlike open(), where O_CREAT being set is the flag used to indicate the presence of the mode argument, there's no way to use the strings passed to fopen() to indicate the absence or presence of an extended argument of any type without extending the contents of these string beyond the existing standard values, and it would change the function prototype for everyone, with potentially unknown consequences for already-compiled code. And if you're going to do that all that...Kimmie
@AndrewHenle But could it not have been like that from the beginning?Farceur
@user16217248 — yes, it could have been like that from the beginning, but it wasn't. At the moment, history is not changeable, sadly (though there are plenty of people trying to revise our interpretation of history, and altering what's printed in books, etc.).Bebel
B
9

Dennis Ritchie (in 1993) wrote an article about the history of C, and how it evolved gradually from B. Some of the design decisions were motivated by avoiding source changes to existing code written in B or embryonic versions of C.

In particular, Lesk wrote a 'portable I/O package' [Lesk 72] that was later reworked to become the C `standard I/O' routines

The C preprocessor wasn't introduced until 1972/3, so Lesk's I/O package was written without it! (In very early not-yet-C, pointers fit in integers on the platforms being used, and it was totally normal to assign an implicit-int return value to a pointer.)

Many other changes occurred around 1972-3, but the most important was the introduction of the preprocessor, partly at the urging of Alan Snyder [Snyder 74]

Without #include and #define, an expression like IO_READ | IO_WRITE wasn't an option.

The options in 1972 for what fopen calls could look in typical source without CPP are:

FILE *fp = fopen("file.txt", 1);       // magic constant integer literals
FILE *fp = fopen("file.txt", 'r');     // character literals
FILE *fp = fopen("file.txt", "r");     // string literals

Magic integer literals are obviously horrible, so unfortunately the obviously most efficient option (which Unix later adopted for open(2)) was ruled out by lack of a preprocessor.

A character literal is obviously not extensible; presumably that was obvious to API designers even back then. But it would have been sufficient (and more efficient) for early implementations of fopen: They only supported single-character strings, checking for *mode being r, w, or a. (See @Keith Thompson's answer.) Apparently r+ for read+write (without truncating) came later. (See fopen(3) for the modern version.)

C did have a character data type (added to B 1971 as one of the first steps in producing embryonic C, so it was still new in 1972. Original B didn't have char, having been written for machines that pack multiple characters into a word, so char() was a function that indexed a string! See Ritchie's history article.)

Using a single-byte string is effectively passing a char by const-reference, with all the extra overhead of memory accesses because library functions can't inline. (And primitive compilers probably weren't inlining anything, even trival functions (unlike fopen) in the same compilation unit where it would shrink total code size to inline them; Modern style tiny helper functions rely on modern compilers to inline them.)


PS: Steve Jessop's answer with the same quote inspired me to write this.

Possibly related: strcpy() return value. strcpy was probably written pretty early, too.

Beefcake answered 10/7, 2018 at 11:38 Comment(4)
Why can fseek use integer constants (SEEK_SET, SEEK_CUR, SEEK_END) but not fopen?Farceur
@user16217248: Good question; I'm curious whether it existed in Lesk's original code that became C stdio. If it was added later, that would be the obvious reason. Otherwise perhaps early code did at some point have to use magic constant integers before CPP existed. Or possibly it was used less and wasn't as painful to change.Beefcake
Anyways I think they should have changed the fopen interface as soon as the named constants became available before it was too late (it kind of is now)Farceur
@user16217248: Apparently they put a big emphasis on backwards compat with existing codebases even very very early, for code that was written before C was even standardized. Like x86, backwards compat was perhaps a reason for early success, but is now a burden whose design can't realistically be changed.Beefcake
B
8

One word: legacy. Unfortunately we have to live with it.

Just speculation: Maybe at the time a const char * seemed more flexible solution, because it is not limited in any way. A bit mask could only have 32 different values. Looks like a YAGNI to me now.

More speculation: Dudes were lazy and writing "rb" requires less typing than MASK_THIS | MASK_THAT :)

Bridgetbridgetown answered 25/3, 2010 at 20:58 Comment(12)
Why though? Masks seem the natural choice here, especially in the day fopen was designed.Ramey
@GMan: As I speculated in my answer, I'm wondering if programmers' time was seen as the more valuable resource. But I have no real proof.Penitence
@GMan: I'm not sure masks are necessarily a more natural choice than a string descriptor. Either are reasonable, defensible design options.Alterable
"unfortunately"? how often does it really annoy you?Markham
I'm going to check this answer, simply because it seems like the most likely explanation. I like Michael Burr's answer as well, but as he said, this is much less likely to have crossed Mike Lesk's mind I imagine. In retrospect, although this question does probably have one definitive answer, I can't really verify anything to make a correct selection! My mistake.Hisakohisbe
"A bit mask could only have 32 different values" -- When C was invented, a bit mask could only have 16 different values.Lobworm
@Windows programmer: No, C does not have a bit mask type. Any integral type will work (preferably unsigned), and unsigned long is a minimum of 32 bits.Belfry
When C was invented, the second parameter to open() had type int, int had 16 bits, unsigned didn't exist, and long didn't exist. The second parameter to open() was an int that was used as a bit mask. Since the int was being used as a bit mask, 16 bits could represent 16 different values.Lobworm
And note that back in the era when the standard I/O library was defined and described (Version 7 Unix and earlier), the open() system call only took two arguments. If open() failed because the file didn't exist, you had to call creat() with the name and mode to create the file.Bebel
Note too that const was not a keyword in C for more than a decade after the (pre-standard) Standard I/O library was written. When the C standard was written, it became possible to state that the C library functions would not modify the strings passed as arguments.Bebel
But the number of possible open flags can easily fit in even 16 bitsFarceur
@user16217248 But the number of possible open flags can easily fit in even 16 bits No they can not. There are 30 different options just for recfm there.Kimmie
P
4

I must say that I am grateful for it - I know to type "r" instead of IO_OPEN_FLAG_R or was it IOFLAG_R or SYSFLAGS_OPEN_RMODE or whatever

Polypropylene answered 25/3, 2010 at 21:9 Comment(8)
This is actually a really good point, it is a lot easier to remember the API.Bridgetbridgetown
I must say I disagree with you. I should be able to tell what a function call does by looking at it's arguments. You can't do that with just an "r". What does "r" mean? You can't tell without reading docs on the function in question.Endodontics
@Tuomas Pelkonen: It's easier to remember to write, but it's a pain in the butt later when you read. Since code is read much more often than it is written, I'd optimize for the read case rather than the write case.Endodontics
I agree that code is read more often than it is written (I actually wrote a blog about this), but to me the readability is pretty good when there is an "r", but I have programmed with C for a long time...Bridgetbridgetown
@BillyONeil: Seems to me to 6 of one half-dozen of the other - I'd need to either know to use "r" or whichever enum corresponds to 'read-only'. I'd say "r" is somewhat easier to remember over some arcane enum name (which is pm100's point). But either way, you need to know the correct thing to pass. If the API were specified to take an enum there would be nothing to protect you from incorrectly passing an arbitrary and potentially meaningless or incorrect integer.Alterable
"What does "r" mean". What do you mean, what does "r" mean? It doesn't mean "rhubarb" or "reverse", or "randomly", I can tell you that. What does the "f" in fopen mean? Is there any serious danger that someone familiar with C could look at a call to fopen and think, "I wonder if this is opening a football in rabid-wombat mode"? Names of standard library functions and arguments can be terser than third-party libraries or code used only for one module, because programmers make an effort to learn standard libraries, they don't just trip over it one day unexpectedly.Deplore
You know to type "r", but if you mistype it and your code says "f" instead, your code will still compile even though it's wrong. If you use named constants instead, and you mistype IO_READ as IO_FEAD, you get a compile error to alert you to the problem.Sedillo
@Wyzard: That's a good point. [And plus, with flags, it would be so much easier to perpetuate the "cryptic-C programmer stereotype" with calls like fopen("x", 4); ;D]Hisakohisbe
A
4

I'd speculate that it's one or more of the following (unfortunately, I was unable to quickly find any kind of supporting references, so this'll probably remain speculation):

  1. Kernighan or Ritchie (or whoever came up with the interface for fopen()) just happened to like the idea of specifying the mode using a string instead of a bitmap
  2. They may have wanted the interface to be similar to yet noticeably different from the Unix open() system call interface, so it would be at once familiar yet not mistakenly compile with constants defined for Unix instead of by the C library

For example, let's say that the mythical C standard fopen() that took a bitmapped mode parameter used the identifier OPENMODE_READONLY to specify that the file what today is specified by the mode string "r". Now, if someone made the following call on a program compiled on a Unix platform (and that the header that defines O_RDONLY has been included):

fopen( "myfile", O_RDONLY);

There would be no compiler error, but unless OPENMODE_READONLY and O_RDONLY were defined to be the same bit you'd get unexpected behavior. Of course it would make sense for the C standard names to be defined the same as the Unix names, but maybe they wanted to preclude requiring this kind of coupling.

Then again, this might not have crossed their minds at all...

Alterable answered 25/3, 2010 at 21:56 Comment(0)
B
4

The earliest reference to fopen that I've found is in the first edition of Kernighan & Ritchie's "The C Programming Language" (K&R1), published in 1978.

It shows a sample implementation of fopen, which is presumably a simplified version of the code in the C standard library implementation of the time. Here's an abbreviated version of the code from the book:

FILE *fopen(name, mode)
register char *name, *mode;
{
    /* ... */
    if (*mode != 'r' && *mode != 'w' && *mode != 'a') {
        fprintf(stderr, "illegal mode %s opening %s\n",
            mode, name);
        exit(1);
    }
    /* ... */
}

Looking at the code, the mode was expected to be a 1-character string (no "rb", no distinction between text and binary). If you passed a longer string, any characters past the first were silently ignored. If you passed an invalid mode, the function would print an error message and terminate your program rather than returning a null pointer (I'm guessing the actual library version didn't do that). The book emphasized simple code over error checking.

It's hard to be certain, especially given that the book doesn't spend a lot of time explaining the mode parameter, but it looks like it was defined as a string just for convenience. A single character would have worked as well, but a string at least makes future expansion possible (something that the book doesn't mention).

Breakneck answered 15/7, 2015 at 18:58 Comment(0)
D
3

Dennis Ritchie has this to say, from http://cm.bell-labs.com/cm/cs/who/dmr/chist.html

In particular, Lesk wrote a 'portable I/O package' [Lesk 72] that was later reworked to become the C `standard I/O' routines

So I say ask Mike Lesk, post the result here as an answer to your own question, and earn stacks of points for it. Although you might want to make the question sound a bit less like criticism ;-)

Deplore answered 25/3, 2010 at 23:11 Comment(3)
There was no such datatype as const char * when Lesk wrote that package. No wait. Ambiguously the datatype might or might not have existed depending on how the implementation handled string constants, but there was no way for a C programmer to specify that datatype in a program.Lobworm
True, but then const didn't exist when strlen was invented, either, but I don't think we can conclude from this, that strlen probably originally took any parameter other than a string pointer ;-) It's just that the typical way of saying "a string" changed.Deplore
According to the same document, CPP didn't exist when Lesk wrote the library. That eliminates the open(2) style of flags ORed into an integer, so a string is one of the least-bad options. See my answer.Beefcake
C
3

The reason is simple: to allow the modes be extended by the C implementation as it sees fit. An argument of type int would not do that The C99 Rationale V5-10 7.19.5.3 The fopen function says e.g. that

Other specifications for files, such as record length and block size, are not specified in the Standard due to their widely varying characteristics in different operating environments.

Changes to file access modes and buffer sizes may be specified using the setvbuf function (see §7.19.5.6).

An implementation may choose to allow additional file specifications as part of the mode string argument. For instance,

file1 = fopen(file1name, "wb,reclen=80");

might be a reasonable extension on a system that provides record-oriented binary files and allows a programmer to specify record length.

Similar text exists in the C89 Rationale 4.9.5.3

Naturally if |ed enum flags were used then these kinds of extensions would not be possible.

One example of fopen implementation using these parameters would be on z/OS. An example there has the following excerpt:

   /* The following call opens:                                                 
              the file myfile2.dat,                                             
              a binary file for reading and writing,                            
              whose record length is 80 bytes,                                  
              and maximum length of a physical block is 240 bytes,              
              fixed-length, blocked record format                               
              for sequential record I/O.                                        
   */                                                                           

   if ( (stream = fopen("myfile2.dat", "rb+, lrecl=80,\                         
      blksize=240, recfm=fb, type=record")) == NULL )                           
      printf("Could not open data file for read update\n");      

Now, imagine if you had to squeeze all this information into one argument of type int!!

Coan answered 30/12, 2019 at 7:50 Comment(0)
P
0

As Tuomas Pelkonen says, it's legacy.

Personally, I wonder if some misguided saps conceived of it as being better due to fewer characters typed? In the olden days programmers' time was valued more highly than it is today, since it was less accessible and compilers weren't as great and all that.

This is just speculation, but I can see why some people would favor saving a few characters here and there (note the lack of verbosity in any of the standard library function names... I present string.h's "strstr" and "strchr" as probably the best examples of unnecessary brevity).

Penitence answered 25/3, 2010 at 21:3 Comment(5)
The lack of verbosity in the library names is because they wanted to support systems that only supported 6 significant characters in external names. Remember C was defined, long, long ago, and not all systems had great tool support.Alterable
Good point, though that doesn't explain fprintf and sprintf. I guess those could have been defined later, though, and I'll admit I'm too lazy to look up the history at the moment.Penitence
@Platinum: fprintf and sprintf becaus they had to be distinct in the 1st 6 characters. Symbols were allowed to be longer than 6 characters, but had to be distinct if the characters after the 6th were dropped. So, I suppose they could have made them more human readable as long as they started with 6 characters of potential gobbledygook.Alterable
@Martin, I could type as fast (or faster) on a teletype than I do today. Computers have gotten faster, I've gotten slower.Divot
@Michael Burr: The general rule was that names with external linkage had to be unique within the first six characters, not counting case differences. This was continued in C89, although the Rationale describes the decision as "most painful".Belfry

© 2022 - 2024 — McMap. All rights reserved.