C Strings and my slow descent to madness
deusinmachina.net
deusinmachina.net
No it doesn’t.
RETURN VALUES
The strcmp() and strncmp() functions return an integer greater than, equal
to, or less than 0, according as the string s1 is greater than, equal to,
or less than the string s2. The comparison is done using unsigned
characters, so that ‘\200’ is greater than ‘\0’. int d = a[i] - b[i];
if (d == 0) return d;It is not that difficult in practice.
The old C std lib is, in my opinion, outdated, obsolete and a very bad fit for complex string handling, especially on the memory management side.
In my own framework, the string management module is using a dedicated memory allocator and a "high level" string API with full UTF8 support from the start.
As a general rule, I think that the C std lib is the weakest part of the C language and it should only be used as a fallback.
Libc string functions don't manage memory. They can be used no matter where your strings are stored. It is more of a choice between generality vs convenience in common cases.
Passing an allocator (like Zig) or a container (like in my framework) to anything that needs to allocate some memory to store a result is both explicit, low overhead and quite convenient in practice.
I've been musing for a while now: what would it look like if we were to discard the C library and design a new one, leaving the language itself intact?
C is not perfect, there are some parts of the syntax that I strongly dislike, like casting or function pointers declaration...
But it is overall a good enough syntax, much simpler than C++.
You often end up with some kind of structure, or variations of structures, for strings:
struct string {
size_t length;
char data[];
};
struct string {
size_t length;
size_t alloc;
char *data;
};
Those are just examples. The tricky part is figuring out the different ownership use cases you want to solve. Because C gives you so much freedom and very little in the standard library, you end up with a lot of variations. You might use reference-counted strings, owned buffers, or string slices, etc. You might want certain types to be distinguished at compile-time and other types to be distinguished at run-time.An example can be found in the Git source code.
https://github.com/git/git/blob/master/strbuf.h
The history of changes to this file is interesting as well. This is a relatively nice general-purpose string type—you can easily append to it or truncate it.
I've seen many libs using this style of strings, not convinced by the practicality.
If you’re not convinced of the practicality, it sounds like you are simply not convinced of the practicality of doing string processing in C at all, which is a fair view point. String processing in C is somewhat a minefield. Libraries like Git’s strbuf are very effective relative to other solutions in C, but lack safety relative to other languages.
The trick is to pass an allocator (or container) to string handling functions.
If/when I want to get rid of all the garbage I reset the container/allocator.
I’ve seen similar approaches, e.g. with APR pools, and if your application can work within those restrictions, it’s very convenient.
Building blocks for memory were also very different from stdlib, notably the use of Handles, which were pointers of pointers, so that the OS could move a block of data around to defragment the heap behind your back without breaking the memory addressing.
C++ string_view is closer to the Right Thing™ - a slice, but C++ doesn't (yet) define anywhere what the encoding is, so... that's not what it could be. Rust's str is a slice and it's defined as UTF-8 encoded.
Back then it wasn't clear which encoding method would turn out to be dominant, so we did all three. (Java was built on UTF-16.)
As it eventually became clear, UTF-8 is da winnah, and the other formats are sideshows. Windows, which uses UTF-16, is handled by converting UTF-8 to -16 just before calling a Windows function, and converting anything coming back to UTF-8.
D doesn't distinguish between a string and a string view.
Imagine you go to a library and insist on borrowing "My Cousin Rachel", but they don't have it. "Oh I don't care whether you have the book, I just want to borrow it" is clearly nonsense. If they don't have it, you can't borrow it.
> D doesn't distinguish between a string and a string view.
In C++ std::string owns the buffer and std::string_view borrows it. If there is no difference between the two in D, then how is this difference bridged?
They added a setting in Windows 10 to switch the code page over to utf-8 and then in Windows 11 they made it on by default. Individual applications can turn it on for themselves so they don't need to rely on the system setting being checked.
With that you can, in theory, just use the -A variants of the winapi with utf-8 strings. I haven't tried it out yet as we still support prior Windows releases but it's nice that Microsoft has found a way out from the utf-16 mess.
I don't mind seeing UTF-16 fade away. We've been considering scaling back the D support for UTF-16/32 in the runtime library, in favor of just using converters as necessary. We recommend using UTF-8 as much as practical.
And the wheels fall off with the first string longer than 255 characters.
However, Free Pascal has the worst documentation of any major project I've ever encountered (The exact opposite of Turbo Pascal), so I can't link to a good reference. Their Wiki is a black hole of nuance and sucks all useful stuff off the internet.
I believe this is used by Redis.
A Freudian slip, methinks.
my_function(my_var, 3.6, "bzarflo", my_other_var, false);
The string handling functions are part of the story, but the null-terminated char * is produced when the compiler reaches a string literal, and writing code without being allowed to just use string literals when it's convenient tends to feel like coding with oven mitts on. my_function(my_var, 3.6, $("bzarflo"), my_other_var, false);
Isn't that much more of a mouthful, and as long as 'my_function' knows to free it, then you're A-OK! The only trouble is '$()' isn't legal in standard C, so a real solution would have to be something like 'str()'.[1] https://www.digitalmars.com/articles/C-biggest-mistake.html
float m[10][10];
it not a an array of pointers, but a 2D dimensional array with 2D memory layout.
int[][] jagged; // an array of `int[]` (i.e. each element is a pointer to a `int[]`)
int[,] multidimensional; // a "true" 2D array laid out in memory sequentially
// allocate the jagged array; each `int[]` will be null until allocated separately
jagged = new int[][10];
Debug.Assert(jagged.All(elem => elem == null));
for (int i = 0; i < 10; i++)
jagged[i] = new double[10]; // allocate the internal arrays
Debug.Assert(jagged[i][j] == 0);
// allocate the multidimensional array; each `int` will be `default` which is 0
// element [i,j] will be at offset `10*i + j`
multiDimensional = new double[10, 10];
Debug.Assert(multiDimensional[i, j] == 0); int N = 10;
char buf[N] = { };
auto x = &buf;
and 'x' has a slice type that automatically remebers the size. This works today with GCC / clang (with extensions or C2X language mode: https://godbolt.org/z/cMbM57r46 ).We simply can not name it without referring to N and we can also not use it in structs (ouch).
How is this not a quality of implementation issue? Any implementation is free to track all sizes as much as they want with the current standard.
Either a implementation is forced to issue an error at run time if there is an out of bounds read/write and in that case its a very different language than C, or its feature as-if lets any implementation ignore.
https://godbolt.org/z/qh7P93Tcd
And I agree that this is a misuse of auto. I only used it here to show that the type we miss already exists inside the C compiler, we simply can name it only by constructing it again:
char (buf)[N] = ...
but we could simply allow
char (buf)[:] =
and be done (as suggested by Dennis Richtie: https://www.bell-labs.com/usr/dmr/www/vararray.pdf)
...and has been since most of us ever used C.
I think one of the major failings of C was the lack of a good standard library that updated with the times.
Actually, I believe a rich standard toolbox was one of the best features of python, and helped with its success.
(Or maybe I’m misinterpreting your comment?)
First part of my comment relates to C library in general, second part of my comment refers to strings and arrays, even if not explicitly.
unsigned int str_len(const char *s)
{
register const char *t;
t = s;
for (;;) {
if (!*t) return t - s; ++t;
if (!*t) return t - s; ++t;
if (!*t) return t - s; ++t;
if (!*t) return t - s; ++t;
}
}
I still use this instead of stdlib strlen. Of course I also use software everyday that I know uses stdlib strlen. For most C programs dealing with strings I just use flex and yyleng, which in turn uses the stdlib strlen. Using flex for small jobs is overkill but it's quick and convenient. I am a hobbyist programmer; I write so-called "trivial" programs.That said, this exact function is used in some "non-trivial" software written by someone else and that person is IMHO a better C programmer than any HN commenter I have seen, most of whom do not let the public see the code they write anyway. Go figure.
NB. I am not the author; this is in the public domain. The author is djb.
- This function will return the number of bytes, not of characters or codepoints. - str and len are both abbreviations, we should use full words when possible - We can also be more explicit about what the function does, it does not simply returns the string length, it counts characters (or bytes in this case)
Here is how I would name it:
u32 CountBytesInString(char* string); u32 CountCharactersInString(char* string);
And on the implementation side, this work can be done with SIMD instructions, and be really freaking fast, but still, it should be explicit for the user that the work is O(n) complexity, not exactly free.
u32 and char are abbreviations.
strlen is also pretty unambiguous, but I still have to check what strstr means.
- Makes it clear that this returns the number of bytes, assuming a naming convention where `sz` refers to size (in bytes) and `ln` refers to length (in some other unit which would be specified in the type). Note that in C, 'characters' refers to bytes. It's a flaw in how C names its types, yes, but I wouldn't say it should be any different just because other languages do things differently.
- It doesn't use full words because I don't think it needs to. Abbreviations are OK as long as every (invested) party agrees that they're sane, and I think they're pretty sane.
- It makes it explicit that it is performing a calculation (hence, is O(n)) via 'get'.
I don't think all this is necessary, though - I actually think 'strln()' is enough. First, because characters means bytes, I can assume that this function is getting the number of characters (bytes) in a string. I wouldn't expect it to give me anything else! Second, in C, if strings were a struct of some sort, I'd expect to be able to get their length via 'str->ln', which would be O(1). The fact that the length is found through a function in the first place signals to me that it's doing something behind the scenes to figure that out. Remember - that's just my opinion, which I admit is extreme - but I think yours is just as extreme.
I think that full words and verbs are easier to read and avoid ambiguity.
I guess this is a matter of style and preference.
This anecdote reminds me of the Mutazt type, something I found in a new codebase I was asked to debug. I had to dig for almost an hour to find exactly what this type was.
Turns out it was a char*, a C string. Buried under 4-5 levels of abstractions.
Mutazt = Mutable ASCII Zero Terminal.
This sort of practice is very outdated. The last time it made sense for performance reasons was probably the early 90s or earlier.
Additionally, strlen is not one of the C string functions you want to replace due to defects, the same way you would want to do with crusty old strcpy. If you're working with C strings there is nothing wrong with strlen. (Just don't call it redundantly in a loop body ...)
Default libs are sometimes very optimized but very often they are not, unfortunately.
If you care about performance, you should not rely blindly on the the defaults.
A long time ago I wondered about the performance of memcpy on the Nintendo DS, for sure they would have provided a hand optimized version? And yes, it was handcrafted ARM assembly code, but my own version turned out to be twice as fast.
They simply forgot to use a simple prefetching trick in their implem.
I'm going to have to doubt the start of your comment. Having seen a lot of libc implementations I think you are better off not wasting time optimizing strlen. Also memcpy, probably memcpy moreso. Most memcpy()s I've seen in the current century are using SIMD instructions and the like. And compilers don't even bother emitting a call to libc for it anymore, they do it as a builtin.
I got the prefetching trick from Intel source code, except that I replaced the PLD instruction by a simple dummy load.
And about strlen, you'd be surprised, some implems are very good, and some are not, depends on the compiler and the library. I've ran benchmarks, I was surprised too.
To be honest, I don't really need super fast strlen, but I was curious and also learning to write fast SIMD code, basic string handling is a nice exercise.
By contrast, many more people are updating optimizations to GCC or clang for arm, more frequently and over a longer timeframe.
GCC and Clang are very nice compilers, but they are a different thing than the std lib. glibc, musl, the Windows C Runtime, iOS, Android, all have different implementations, sometimes outdated.
Same is true of MSVC.
It's been that way on most modern compilers for about 20 years.
That's why I'm saying rolling your own may be futile. Compilers, not just libc, have paid a lot of attention to getting those things fast.
But testing is always there to the rescue, and these days we have Godbolt.
But I have not published those benchmarks, if this is what you're asking, the Nintendo thing I am afraid I cannot reproduce easily as I no longer have this devkit on hand.
About the strlen benchmark, this is something I've done a few years ago, that could be easy to run again, but I am not sure this is worth the effort just to convince a random dude on the internet...
Yes. Share the data or you're spreading FUD.
https://git.musl-libc.org/cgit/musl/plain/src/string/strlen....
#include <string.h>
#include <stdint.h>
#include <limits.h>
#define ALIGN (sizeof(size_t))
#define ONES ((size_t)-1/UCHAR_MAX)
#define HIGHS (ONES * (UCHAR_MAX/2+1))
#define HASZERO(x) ((x)-ONES & ~(x) & HIGHS)
size_t strlen(const char *s)
{
const char *a = s;
#ifdef __GNUC__
typedef size_t __attribute__((__may_alias__)) word;
const word *w;
for (; (uintptr_t)s % ALIGN; s++) if (!*s) return s-a;
for (w = (const void *)s; !HASZERO(*w); w++);
s = (const void *)w;
#endif
for (; *s; s++);
return s-a;
}https://cr.yp.to/ucspi-tcp/install.html
https://cr.yp.to/daemontools/install.html
https://cr.yp.to/djbdns/install.html
https://cr.yp.to/clockspeed.html
https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=strin...
#include <libc-pointer-arith.h>
#include <string-fzb.h>
#include <string-fzc.h>
#include <string-fzi.h>
#include <string-shift.h>
#include <string.h>
#ifdef STRLEN
# define __strlen STRLEN
#endif
/* Return the length of the null-terminated string STR. Scan for
the null terminator quickly by testing four bytes at a time. */
size_t
__strlen (const char *str)
{
/* Align pointer to sizeof op_t. */
const uintptr_t s_int = (uintptr_t) str;
const op_t *word_ptr = (const op_t*) PTR_ALIGN_DOWN (str, sizeof (op_t));
op_t word = *word_ptr;
find_t mask = shift_find (find_zero_all (word), s_int);
if (mask != 0)
return index_first (mask);
do
word = *++word_ptr;
while (! has_zero (word));
return ((const char *) word_ptr) + index_first_zero (word) - str;
}
#ifndef STRLEN
weak_alias (__strlen, strlen)
libc_hidden_builtin_def (strlen)
#endif
NetBSD common strlen:https://ftp.netbsd.org/pub/NetBSD/NetBSD-current/src/common/...
size_t
strlen(const char *str)
{
const char *s;
for (s = str; *s; ++s)
continue;
return(s - str);
}
Apple strlen:https://opensource.apple.com/source/Libc/Libc-1244.50.9/stri...
Apple strlen comes from FreeBSD. Until 2009, FreeBSD used an unoptimised strlen.
https://svnweb.FreeBSD.org/base/head/lib/libc/string/strlen....
size_t
strlen(str)
const char *str;
{
const char *s;
for (s = str; *s; ++s);
return(s - str);
}
FreeBSD eventually copied^1 NetBSD's x86_64 strlen.1. "modeled after", "inspired by", etc.
https://svnweb.FreeBSD.org/base?view=revision&revision=18770...
#include <sys/cdefs.h>
__FBSDID("$FreeBSD$");
#include <sys/limits.h>
#include <sys/types.h>
#include <string.h>
/*
* Portable strlen() for 32-bit and 64-bit systems.
*
* Rationale: it is generally much more efficient to do word length
* operations and avoid branches on modern computer systems, as
* compared to byte-length operations with a lot of branches.
*
* The expression:
*
* ((x - 0x01....01) & ~x & 0x80....80)
*
* would evaluate to a non-zero value iff any of the bytes in the
* original word is zero. However, we can further reduce ~1/3 of
* time if we consider that strlen() usually operate on 7-bit ASCII
* by employing the following expression, which allows false positive
* when high bit of 1 and use the tail case to catch these case:
*
* ((x - 0x01....01) & 0x80....80)
*
* This is more than 5.2 times as compared to the raw implementation
* on Intel T7300 under EM64T mode for strings longer than word length.
*/
/* Magic numbers for the algorithm */
#if LONG_BIT == 32
static const unsigned long mask01 = 0x01010101;
static const unsigned long mask80 = 0x80808080;
#elif LONG_BIT == 64
static const unsigned long mask01 = 0x0101010101010101;
static const unsigned long mask80 = 0x8080808080808080;
#else
#error Unsupported word size
#endif
#define LONGPTR_MASK (sizeof(long) - 1)
/*
* Helper macro to return string length if we caught the zero
* byte.
*/
#define testbyte(x) \
do { \
if (p[x] == '\0') \
return (p - str + x); \
} while (0)
size_t
strlen(const char *str)
{
const char *p;
const unsigned long *lp;
/* Skip the first few bytes until we have an aligned p */
for (p = str; (uintptr_t)p & LONGPTR_MASK; p++)
if (*p == '\0')
return (p - str);
/* Scan the rest of the string using word sized operation */
for (lp = (const unsigned long *)p; ; lp++)
if ((*lp - mask01) & mask80) {
p = (const char *)(lp);
testbyte(0);
testbyte(1);
testbyte(2);
testbyte(3);
#if (LONG_BIT >= 64)
testbyte(4);
testbyte(5);
testbyte(6);
testbyte(7);
#endif
}
/* NOTREACHED */
return 0;
}Such as?
gets(buf);
scanf("%s", buf);
strcpy(buf, argv[1]);
scanf("%80s", buf);
strncpy(buf, argv[1], 80);
snprintf(buf, 80, argv[1]);
----The delightful answer is none of them. The first three have no bounds checking at all, meaning that they will happily overflow the buffer to an arbitrary extent (gets, at least, will usually trigger a warning on modern compilers). The next two have off-by-one errors: scanf will write a NUL byte out of bounds (and that's exploitable! https://googleprojectzero.blogspot.com/2014/08/the-poisoned-...) while strncpy will fail to NUL-terminate the string. The last one uses the right buffer length, but treats user input as a format string and can leak memory contents or produce arbitrary memory corruption with the %n format specifier.
C string handling practically invites off-by-one errors and horrible security practices out-of-the-box.
(this is tongue-in-cheek, the function is still bad IMO because it almost never does what you need it to. If it guaranteed null termination it would be more useful)
snprintf(buf, 80, “%s”, argv[1]);
Should work.-Emily
No. Sorry. This is bad programming. C'mon.
I started programming back in the 8080, 8085, 6502, etc. days. I had to program some prototype computers using a hex keypad while entering raw machine code (not even assembler). I still own a couple of these:
https://i.imgur.com/ZsIJj1p.png
In a couple of cases I had to take this approach to bootstrap Forth on a 6502, then write a full Forth code editor and finally write the robotics application from there.
Do not confuse bad programming or lack of knowledge with something attributable to a language, any language. A knowledgeable software developer, among other things, stays clear of these issues. This is also the value of experience and exposure to a wide range of technologies.
It's like blaming MicroPython for a machine getting destroyed because garbage collection interrupted a critical real time process. There's nothing wrong with MicroPython in that regard, the programmer/designer of the embedded system either lacked knowledge and understanding.
Part of the problem, as I see it, is that a good deal of modern university CS degrees don't even touch low level stuff. They start students on languages like Javascript and Python. These are fantastic, however, someone with deep-rooted experience in these languages who jumps into C is very likely to do some truly horrific things. The language isn't the problem, at all.
I mean, not to go too far, the Linux kernel is written in C. Right? It's about the person, not the language.
I've been writing software in over a dozen languages for over 30 years. Generally speaking, when I write code, even complex code, in any language, it just works. Not because I am something special. I have done a lot of of work across a wide range of application domains and have made my share of mistakes over the years.
Of course I make mistakes. Everyone does. Yet these mistakes. They have nothing to do with lack of domain knowledge. People who approach C without having a clue as to how memory, registers and the internals of a processor and memory system work are going to create bad code.
Blaming the language, the tools, is irrational. You can write perfectly good, safe and performant code in assembler. And boy, can assembler be a minefield in the hands of someone without experience!
Are we going to blame the processor microcode then? No, of course not.
I find that dubious, to say the least. Languages are not all equal, obviously. You can take an existing language and make it worse by removing some useful features of degrading existing ones; so why couldn't you make it better? In the case of C, its string handling has been proven time and time again to be a (collection of) footgun.
I think you are stretching it. Still, let's go with it.
I have done a ton of construction work in my life. From large projects at home ($200K-ish) to managing the build of a $12MM data center I designed. Because of this I have been around construction guys of all kinds and skill levels. And, of course, I have a lot of personal experience doing the work as well, from carpentry to just-about anything in a typical home or commercial project.
Anyhow, I always cringe when I see experienced construction guys work with modified tools that have had safeties removed to make the work go faster. One example of this was when I watched these guys cutting concrete blocks with a handheld grinder. They had removed the guard that typically covers half the blade. The entire blade was fully open and spinning at 10K+ RPM. When asked they said they'd been doing it this way for twenty years, it's faster, they can see the cut and control it far better. Still had all fingers.
Same is true of guys cutting framing lumber with circular saws or skillsaw's while propping-up the pieces with their bodies.
To me, someone with not even 10% of the experience they have, that was unthinkable. I would have lost fingers and limbs. I would have ended-up in the hospital almost instantly and possibly take others with me.
It's a relative term. Are the tools bad? Well, when experienced professionals can use them safely day in and out (this is their job, they've been doing it this way every day for twenty years), can we really blame the tool of I grab it and proceed to remove a finger or three?
No. Of course not. I know the American system of liability doesn't work that way, but that would be and should be 100% my fault for not having the experience necessary to approach such a thing safely.
It's the same thing, it doesn't matter if we are talking about coding, CNC machining or downhill skiing. Newbies love to blame the skis for what they did wrong, or the $150K CNC machine for crashing the $10K spindle into the table. It's never their fault. Sure.
In C everyone starts out as a "bad programmer". In other languages people are merely inexperienced.
>however, someone with deep-rooted experience in these languages who jumps into C is very likely to do some truly horrific things. The language isn't the problem, at all.
You are contradicting yourself.
In everything in life one starts out as a "bad <X>". I would be a bad free diver (I'd probably kill myself).
People inexperienced in <X> lack knowledge in <X>. That is not an insult. That's just reality. One can do some pretty serious mistakes as an inexperienced Python programmer (example: async/await) or downhill skier.
It's not the language, it's lack of knowledge and experience.
It's not the ski's, it's lack of knowledge and experience.
Are we now in a culture where saying that someone is doing <X> badly because they don't have experience is an insult? OK, great. Let's blame everything else, except for lack of experience. C is the problem. Please don't use it.
It will be very interesting to watch as the "not my fault/blame everyone but me" crowd faces having justify their lack of skills with what tools like ChatGPT will evolve into. Very interesting. I guess we'll blame LLM's for not knowing <X> well enough to be hired.
The C code will have silly things, like bad style or `double d = malloc(sizeof(double))` (instead of `double*`), which makes it evident that its training data was full of pretty bad C code. Which makes sense since most C code out there, like on StackOverflow, is bad. Same with Bash code.
The worse quality of code available in these langs suggest to me that these langs are inherently more difficult, which means people are more likely to be bad at them.
Whether they deserve blame for that, or whether it disqualifies them as legitimate technologies, is subjective. Objectively, though, you're accepting a higher rate of failure by using them over less difficult alternatives. If "good <X>" colloquially means "<X> with high likelihood of generating desired outcomes" and "bad <X>" means "<X> with high likelihood of generating undesired outcomes", I think it's fair to call both "bad langs". ;p
Back when OpenAI had code-specific models based on GPT-n they were generally specifically advertised as best at Python; I suspect their coding-related training data and human feedback on coding tasks all favors Python by a significant amount (and I supect that that actually gets reinforced by positive feedback, since this makes it most likely that they get used with Python over time, too.)
It's only a matter of time. And likely not a lot of time.
I asked ChatGPT to write a fast CRC-16 calculation algorithm in ARM assembler given a set of preferred registers and other constraints. I compared it to my own code, written a while back. Not too bad.
It wasn't clever about using assembler tricks experienced assembler coders understand, yet the code passed my test suite. My code was much faster because it was written with the benefit of experience that had me reaching for optimizations ChatGPT did not.
The interesting part was when I asked that it modify the code to work with a different buffer structure and be able to compute CRC-8, CRC-16 and CRC-32 with various modifiers.
It did it in just a few seconds. The code passed 100% of my tests. Not super fast or efficient, but it worked. I remember when I had to do that myself with my own code, it took over a day.
This is today, mid 2023. Give it a year or two (maybe less?) and it will be a tool to contend with. People who like to blame everything else rather than their lack of knowledge and experience will not do very well in that world.
Why would I pay someone to do <X> when they bring nothing special to the table?
Here's the huge paradigm shift (at least for me):
I could not care less what someone knows or does not know. I care about the range and breath of their experience and how they approach learning that which you do not know.
Someone like that can use any available tool, including AI tools, to deliver value in almost any domain. Someone who blames others (tools, people, the system, whatever), cannot.
We might just be entering an era in which experience will be shown to have serious value.
This is the "no true scotsman" fallacy.
Languages can be designed so that less than perfectly knowledgeable programmers fall into the pit of success, or they can be designed so that they fall into the pit of failure.
For people making your argument, I like to provide this challenge: Go take a flight on a 737 MAX that hasn't had its MCAS fixed/disabled. That should be fine, right? After all, no "true" pilot ought to disregard one sentence on page 437 of the flight manual that they weren't even given during a 1 hour training video. A true professional pilot memorises the engineering blueprints, the source code of the avionics, and the wiring schematics, surely. So you have nothing to fear! The plane is "safe", and pilots can be trusted to be knowledgeable.
Go buy that ticket.
Sorry. Not even close. Source: I actually studied Phisolophy/Logic at Uni. Good try though.
Also, your aircraft example is absolutely ridiculous.
This isn't an appeal to purity at all. This is about domain knowledge and experience.
A more appropriate example might be the contrast between someone who has only done 3D printing now deciding to design and make parts meant for CNC machining. The lack of expertise and understanding will result in some pretty serious problem.
Another example, this time about software development. I have over ten years of professional software development using Forth. Someone coming to Forth from, say, Python, is likely to make an mess until they understand how to approach problems in Forth. I also have about ten years professional coding experience using APL. Same thing. Someone coming to APL from other languages is going to run into problems until they gain enough knowledge to write APL.
Appeal to authority.
PS: I studied philosophy too.
I am telling you that I evaluated your claim, I didn’t google it or ask ChatGPT.
This is precisely why C has outstayed its welcome in so many areas of software development.
Every time some kid looking for a self-confidence boost buys into the idea that using a language with a minefield of archaically-named string manipulation functions somehow makes them a ‘real’, ‘smart’, developer, we are all left a little worse off.
No, it’s not the fault of the language’s design. It’s not even the fault of history - the fact that C was conceived at a time when security wasn’t what it is. It’s these damn kids that only know Python and JavaScript! Why can’t they be as smart as us C developers!
This is all completely ignoring the fact that in 2023 we have no shortage of string manipulation-related vulnerabilities in widely popular and supposedly battle-tested C code. All some version of the typical list completely justifiable human errors that anyone is bound to make writing C.
A language that is so popular but that so few people seem to be able to write secure code with, is not a very good language.
I’m immediately skeptical of anyone that’s not of the view that the single best thing we as an industry can do for security is to drastically reduce the amount of C code in circulation. It always comes down to “I’m set in my ways and I think I’m superhuman”.
My hope is that these modern, sensible systems programming languages successfully eat the world faster than the pool of C developers thins out, as people slowly retire, and more greenhorns clue into the fact that C is being used in more places than it ought to be.
Signed, someone that did learn C in school, and has written it professionally.
> ‘real’, ‘smart’, developer
It should not. And you are taking my comment completely out of context. 100% out of context. Violently out of context.
I have not even implied that this is about "real" or "smart" developers. C'mon!
This is about TWO things: Knowledge and experience. And that is IT. That's what I said.
So, pretty please, don't put words in my mouth and get all self-righteous about something you invented.
> Why can’t they be as smart as us C developers!
They can! All they have to do is learn and develop the experience base to use the tool correctly. Nobody is saying it can't be done. Again, don't put words where I did not use them.
Do you drive a car every day?
Yes?
Do you think you would do well if you got in the seat of a Formula 1 car?
Of course you would not. Because you lack knowledge and experience in the domain. You can learn. Of course you can learn. And that requires work and dedication.
Blaming the Formula 1 car for the lack of knowledge and experience of the driver is nothing less than ridiculous.
On the other hand, not everyone had that luck. I've seen a good number of people that are very good at what they do but lack more general culture. But it's hard to keep up with everything. Software is a huge world, I think already way too big for everyone to know everything. And it's not just software too, it's important to learn about business too, and maybe a bit of maths here and there isn't a bad idea, and there's also the hardware part, networking, and every day there is more and more and more.
What I mean by this is that I don't know how things were before, but today, for a lot of people that write code, it's not possible to know everything, have everything fit inside your head. In those cases, people usually start asking for more guardrails in their tools, because they're no longer manipulated only by experts. And sometimes the experts themselves ask for guardrails too. So some want tools to change, others don't want them to change, and both have a point.
On one hand, I understand that blaming the tool isn't a good attitude to have. On the other hand, my job consists in building tools for other professionals, and I feel like I have way higher standards for the tools that I produce compared to the tools that I use.
I think your view is of this is reasonably balanced. There is that element of someone without extensive experience not knowing what they don't know.
Well, can we blame them for that?
Thirty years ago, probably not. Today, I think the answer could be yes. A few days of time well spent web searching, reading and watching videos can bring someone from complete ignorance of a subject to having a very good starting point from which to grow. Today there's information on almost anything anyone might want to learn, free and widely available. What, generally speaking isn't widely present is the willingness and dedication to learn.
I have friends my age who stopped learning twenty years ago, maybe even sooner. They just don't care enough. Or maybe they thought they were safe and did not need to. In at least one case I know, that was a huge mistake. He started life as a field service engineer with great prospects. He never bothered to learn anything new. Today he sits in a trailer at an oil field 24/7 manually logging various pressures and temperatures multiple times a day.
I also blame the educational system for some of this. Maybe I was fortunate to have gone to school when I did. We started with assembler. Actually, machine language, raw 1's and 0's. By the time I learned C I had designed a few industrial control computers and fully coded them in assembler. The transition to C was very easy. And nobody had to tell me where the dangers were...because, coming from assembler, it was obvious.
I've also heard that for them, it can be valuable to have someone with a fresh outlook on things. You notice things that people got used to, and most of the time things make perfect sense considering the situation, but sometimes there's an opportunity to improve things for the better.
You're right about learning, it's a lifelong process. I do think that doing this along other people helps. Sometimes working on something by yourself can be quite lonely, especially if the people around you are not that much into all of that. That kind of loops back into the discussion about tool. Blaming your environment is counterproductive, but it's still important to pick it carefully.
yeaaa, this has much bigger problems than a null write...
At that point it is beating dead horse - it is such well known fact how C strings works. And it's insanity that noone proposed new standard library with better implementation for strings domain. Boo hoo "old programs" bla bla...
And it's total insanity to blame powerfull language for allowing you to do almost anything in it. You don't even need to ask commitee for permission to roll your own _low level constructs_ - how insane is that ? ;)
But keep spitting on what gives you freedom...
Yes it is. And I bet on a modern windows version it is too. The terminal has been (probably intentionally) neglected by ms for a long time, but as far as I know this has mostly been fixed on modern windows versions.
EDIT: Author admits it later in the text "will be fixed in Windows 11 and Windows Server 2022"
Also it says "strlen("有り難う")); [...] and the output is… The length of the string is 12 characters". But according to "man strlen": "RETURN VALUE: The strlen() function returns the number of bytes in the string pointed to by s.". It says nothing about "number of characters".
I don't think it is an intentional lack of care, just a lack of care. Internally MS devs affected by the appalling state of the console just did what the rest of us did and installed an alternative.
> but as far as I know this has mostly been fixed on modern windows versions.
Ish. The default console for powershell is better, but a lot of improvements you might be thinking are in there are in fact only in Windows Terminal (https://en.wikipedia.org/wiki/Windows_Terminal) which is not currently included by default.
A lot of those changes are in ConsoleHost, so Windows 10 and 11 get those improvements (like VT100 sequences) in cmd.exe as well
Yeah - when dealing with Unicode, you have to be very clear about whether you're dealing with bytes, runes or glyphs.
Also in terms of Unicode, graphemes are even more relevant to the programming side than glyphs - unless you’re writing a renderer.
It's still broken unfortunately, you need to switch the console to a special UTF-8 codepage in your own code:
SetConsoleOutputCP(CP_UTF8);
...and before exit restore it to the original code page.It works if you write multiple UTF-8 code-units in one go, but breaks if you send them in several writes (and by that, I mean direct writes to the HANDLE). It also breaks if you try to use the ANSI API (with the A suffix), as it internally tries to convert the bytes from codepage-random to UTF-8.
You run into both issues if you try to use the MS implementation of stdio (printf and friends).
And we didn't even discuss command line argument passing yet :-)
I had a lot of fun with this (more explanation in the issue comments): https://github.com/AgentD/squashfs-tools-ng/issues/96#issuec...
I tried to test it with the only other two languages I know besides English: German and Mandarin. Specifically also, because the later requires multi-byte characters to work. Getting Chinese text I/O to work at all in a Windows DOS box, on an existing, German Windows 7 installation was an adventure on it's own and ended up breaking things in different ways than German text.
Turns out, trying to write language agnostic command line applications on Windows is a PITA.
And programming, engineering, and life in general have so, SO many other situations where "X is not very good at doing Y". Yet (my experience) guys seem extremely resistant to the common-sense strategy of "then try to minimize how much Y you do with X".
Like what?
Didn't take that long to profile (well, printf's as no profiling available) and figure out it was the strncpy's causing the problem, but why? Well, there was a handy 8 megabyte buffer used for working memory that the strings were being copied into that for modification.
From the strncpy() man page:-
>If the length of src is less than n, strncpy() pads the remainder of dest with null bytes.
Ah. So every little strncpy was essentially copying the string then zeroing out 7,999,992 bytes. And there were lots of little strncpy's...
Similar rule for sprintf(), all instances of which should be replaced by snprintf().
snprintf (huge_buf, huge_buffer_size, "%d", 1);
will write two bytes into huge_buf, regardless of huge_buffer_size (assuming it is 2 or larger).what makes you believe it will iterate the whole of huge_string?
> The snprintf function returns the number of characters that would have been written had n been sufficiently large, not counting the terminating null character, or a negative value if an encoding error occurred.
In essence it needs to return strlen of huge_string even though very little of it was actually written.
> Attention: In versions of the GNU C Library prior to 2.1 the return value is the number of characters stored, not including the terminating null; unless there was not enough space in s to store the result in which case -1 is returned. This was changed in order to comply with the ISO C99 standard.
ISO C99 needs a kick in the head. Yes, there is a use case for this return value (buffer wasn't large enough, reallocate and try it again). But wow, own goal team!
Thanks for this. I had no idea that C99 had defined this so stupidly. I do see that in 2004, the linux kernel added replacements (scnprintf() which behave as the pre-C99 versions of snprintf generally did). There's a good discussion of this here: https://lwn.net/Articles/69419/
Or to paraphrase that "We're not in Python any more, and C is not Python".
You know what sends me insane? Indentation and lack of fixed types in Python. But I don't have problems with C strings. Because I have grown to love and know C's string foibles just like the author will certainly not be driven insane by 'Python's shortcomings according to me'.
The world is full of people who complain that something or other is different from what they know, so that 'other' is wrong. That's just being isolationist. Everything has its own advantages, its own disadvantages. Let's accept that and move on, instead of making mountains out of mole-hills.
Whenever I see someone complain about Python's indentation, my brain internally translates it to "I poorly format my code."
If you code is properly formatted, then Python's indentation is never a problem. I praise Python's indentation-as-syntax because it prevents issues like a dangling else or a forgotten brace while also making proper formatting a requirement for your program to run.
Python's rigidity on formatting should solve that problem, but it really doesn't and over time it relaxed the rules somewhat which made it better.
I'm sure you have some solid 3rd party research to cite to back up that absolutist claim. Or have some explanation that a it with my workmate's fault and not a git merge (which mixed up indenting between the two files) that introduced a bug that took hours to track down. Or why people like me loved the idea of meaningful indentation in python, then grew to not love it any more after experience.
Or the bitch that it can be when you have to generate code and have to do more than just slap braces around it to delimit blocks.
All in all, another one of those pure-opinion HN posts that are becoming too common.
People are different, work in different ways, are productive with different techniques, have their own habits formed over decades that they don't feel the need to change. None of that means they're worse than you, who, of course, always properly formats your code.
It's perfectly possible, and not that rare, to be careful with indentation and not liking significant whitespace (esp. if the rules are not overly consistent, like in Python). It's also possible to feel like the need to manually adjust indents after moving the code around is a distracting chore that braces largely eliminate. It's possible to work exclusively within an IDE that will make "dangling else" problem impossible to happen, therefore it's possible not to see that as a problem. And so on.
Keep an open mind. Try to get accustomed to various style of working with code. Stop your brain from being discriminatory, and don't assume too much about what other people consider "good" or "poor". If in doubt, ask politely.
BTW: I'm using Python since 2.5.2 professionally. Just a quick disclaimer.
C's issue are not just harmless foibles. They cause real harm to the poor people actually using the software.
there is no way of fixing this with a "string type". The errors come from IPC/Internet, and there will always be just a sequence of bytes, and some length, maybe, given by the user. Somewhere some code will have to trust this length, or compute a length.
It's not that they are "different", it's that they are extremely error prone and have poor performance for certain operations such as getting the length.
>But I don't have problems with C strings
Everyone thinks they are clever enough to use them and other parts of C without problems, and those people are the most dangerous.
It's not so much a question of different is bad, it's that having one of the six positions for your car's stick shift be marked 'Self-destruct' is... Sub-optimal. I'm sure you're smart enough to operate that car safely, but the ditches seem to be filled with burnt-out husks.
Tab-based, versus curly-brace indentation, on the other hand, is a question of how you want the car painted. Purely personal taste.
Good analogy, but I think you're being to forgiving to C. It's more like having all 6 out of the 6 positions of your car's stick shift marked as self-destruct. If you don't want the car to self-destruct while changing gears, you need to tune your FM radio to 99.0 Mhz and quickly set your turn signals to left, right and left again before shifting. And that only works safely when shifting into the 1st, 2nd and 4th gears, unless you modify your car engine sot it can only drive on Microsoft roads.
They serve the exact same purpose, and are both necessary, but the choice of braces versus whitespace is purely aesthetical.
But it doesn't have to stay that way. Someone else in the comments mentioned antirez's sds library for dynamic strings. This works, but you could also easily roll your own. All you need is an init function, and perhaps an assert or other check at the end of it that the string has a nul terminator.
At that point, type checking will let you blindly pass those strings (or their char arrays) to any of those C functions without worry.
Edit: I'll also add that I think a string library should have a difference between static strings and string builders (dynamic strings). It makes everything easier.
C strings aren't bad. They can't be, because they don't exist. C doesn't have strings. And that is the issue.
As you say, things get a lot better when you actually introduce strings as a concrete concept rather than a set of lose conventions.
So that strcmp expects a string rather than a pointer to a character is just convention. In languages which actually has strings as a concrete concept, like say Pascal and derivatives, you can actually differentiate between those two cases.
[1]: https://www.gnu.org/software/gnu-c-manual/gnu-c-manual.html#...
char s[3] = "foo"; // not null-terminated!Byte count.
In practice, in C context, character == char == byte. Other concepts have to use different names to avoid confusion with the language spec.
C has string literals though, and those bake a specific string representation into the language (of course libraries can use their own string representation, but those then need at least some conversion function from string literals).
WUFFS doesn't have strings. That's what a language which doesn't have strings looks like, you can't write "Hello, world" in WUFFS because it involves Strings, which WUFFS doesn't have, and I/O, which WUFFS also doesn't have.
A pretence that C doesn't have strings because it lacks a concrete string type in the language itself also seems like you'd be claiming C++ doesn't have strings, Zig doesn't have strings, and Rust came pretty close to not having strings (for a while it was mooted to make Rust's str just a slice [u8] but today Rust does bless str as a distinct type even though e.g. &str and &[u8] aren't very different)
Zig and Rust I don't know enough about.
And I'm not pretending. C has string literals which are of a non-distinct type. You can't distinguish between a string literal and an array of characters. This is the crucial bit.
The result is that the standard library, and lots of other code, relies on convention alone to pass strings around. This has been and continues to be the source for countless serious bugs. The kind of bugs which are a total non-issue in languages which has strings.
[1]: https://en.cppreference.com/w/cpp/language/user_literal
const char *s = "Hello World!";
const char *world = s + 6;
size_t world_len = 5;
printf("%.*s\n", world_len, world);And if you need to use C anyway, then please use anything else than the string functions from the standard library. The C stdlib is (mostly) a leftover from the K&R era when opinions about what makes a good API were very different from today, and C was a much 'harsher' language.
C is pretty nice for a lot of things, but working with strings definitely isn't one of them.
You can always allocate a very large buffer and do your string operations there, using memncpy and the assorted functions which can be inlined in many architectures and be really fast.
Then you can dispose of the buffer really quickly with one call or reuse it for later operations by simply setting a few pointers to initial status...
The C stdlib is basically the lowest common denominator which enables writing very simple UNIX-style mostly-cross-platform command line tools, but not much else. For anything serious you either call OS API functions directly, or resort to specialized third-party libs.
Attempting to 'fix' the stdlib would first mean agreement on what such a stdlib should actually contain and look like, and this has a real risk of ending up in C++ Commitee style busywork (e.g. lots of activity with little to show for).
I’ve also been having a blast with C because writing C feels like being a god! But the biggest thing that I like about C is that the world is sort of written on it!
Just yesterday I needed to parse a JSON… found a bunch of libraries that do that and just picked one that I liked the API.
Not trying to be a troll but as someone who has also written a lot of C in the past why do you feel like this?
As when I’d pick Go because I was doing some concurrency, I can now explore a bunch of concurrency libraries, including some implementations that look a lot like Go channels. Want to watch a file for changes? I can do that all the way from taking to the kernel to picking a multi-platform library. I guess, I haven’t really found anything that I can’t do in C, and if I’m lazy, I can just embed and scripting language to handle things in a higher level. Macros are also very powerful! I’ve been writing code that writes code for me, export the thing to a .h file and import it using #include.
pkg-config —list-all has become my friend and I keep discovering that the world is written in C and the access to libraries is huge!
Also, idiomatic C is whatever you make it. There are a bunch of ways to skin a cat. Want a different platform? Build tool? Compiler? Debugger? Wanna write you own debugger? C is chill with that.
It’s also such a simple language that without much effort you can know everything about it (I don’t care that much about anything over C99). I don’t know the whole ecosystem, or standard libraries, or data structures and algorithms and whatnot, but the language itself is quite trivial.
With that said, I’m not using C in project teams. In that setting some strong conventions would likely be necessary or even better, something enforced by tools or the compiler (like Go), but yeah, I’ve been quite enjoying working with C and being kind of annoyed at other langs that I need to use for work because they keep doing all this stuff behind my back that is supposed to help me, but instead is a pain trying to debug and understand what is actually happening
- Access and control, nothing "behind your back"
- Low or high level, as you prefer
- Swap in different implementations (custom allocator, different async runtime, etc)
- Really powerful macros
- Strong conventions, safe by default (but you can break them, go into the weeds if needed)
Downsides compared to your list:
- More complex than C or Go (though less than C++)
- Only one production compiler, and everyone assumes `cargo` build system (though both are very good)
- Library ecosystem not quite as extensive (though there is a lot of good stuff on crates.io, and you can always write bindings to C)
The little things that seal the deal:
- Enums (tagged unions without the danger or boilerplate)
- Zero-cost closures
- Incremental compilation
- "If it compiles, it probably works"
- Standardized documentation via `rustdoc`
- Module system
Yes, I've tried rust and have even shipped some project with it! I think the things that didn't worked for me was the complexity. It felt like I had to keep a lot of things in mind to be effective, (traits can be a bit obscure, i.e. magic IMHO), also lifetimes and Option made the code complex by either having a bunch of math or .unwrap all over the place.
With that said tho, Rust would be one of my top picks for a professional setting or a codebase that I share with a team, because of the really good defaults that it has! I read once somebody comparing the Rust compiler with a bunch of tiny unit tests that the developer doesn't have to write, and I agree with that!
With C tho, for personal stuff I can do things that I've never do professionally (i.e. use the OS as my GC because when the process dies the OS is gonna "free" my memory allocations anyways! I know, terrible, but I'm having fun! ¯\_(ツ)_/¯)
However, I don't think that's the best option if you can roll your own.
I personally think that there should be two different types of strings: static and dynamic ones. The static ones should not be able to be changed, but the dynamic ones can serve as a "string builder" type of sorts.
Second, I don't see sds's first advantage (in the README) to be an advantage. Sure, you may have to explicitly pass the buffer in to C functions, but that tells you that you're calling a function that takes a char array rather than your string. It makes it more explicit.
Second, if you use my method of splitting static strings from dynamic strings, then sds's second advantage doesn't apply because the pointer will never change.
But the disadvantages of sds still apply, and both disadvantages are big since they easily lead to bugs. Hence, I think sds is not the best option if you can make your own.
Oh, another advantage of the static/dynamic split: I can implement small string semantics. For small enough strings, I use a union to put the array into the same bytes as the pointer, so on 64-bit machines, I can have 8-byte strings (including nul) before needing an actual allocation.
That library really doesn't address any of the Unicode issues.
If you want Unicode in C a wrapper library is pretty much a given.
When I was adding Unicode support to the small scheme interpreter I like playing with I found a super simple string library, a bunch of generated code (who doesn’t love 1000 line switch statements) for dealing with utf-8 code points and bob’s your uncle. Could have probably found a library that did it all but the goal was learning and yak shaving.
Haven’t ever messed with the wide strings, seems like more of a hassle than they’re worth.
That's funny, because I look at it in the opposite way: it makes me feel like a super-fallible human because it's so easy for me to break things in horrible ways, something a hypothetical god would not do.
Something like Rust &str or String would make me feel more like a god, as I can do whatever I want (more or less) without worrying about safety.
I think that's the idea - they're not reveling in the fact that they can do anything anybody could reasonably want to do, they're reveling in the fact that they can also do everything else, too.
I think the reason people don't want to standardise this kind of function is it often gives wrong behaviour. for example if you are trying to copy a string into a fixed buffer and its too long then often it is an error or potentially even a security bug to truncate it. so these functions generally do the 'wrong' thing even though they are 'safer'. if you are dealing with static buffers then I think you should be explicitly checking the source fits in the target and then handling the error case. you could even have a function like `strlcpy` that does `strlen` then checks if it fits, then does the copy or return an error code. alternatively, if the string should always fit and you don't want to handle the error case then the safe thing to do is check at runtime that it fits then abort the program if it doesn't fit.
In fact, strlcpy is worse:
* strlcpy truncates the source string to fit in the destination (which is a security risk)
* strlcpy does not perform all the runtime checks that strcpy_s does
* strlcpy does not make failures obvious by setting the destination to a null string or calling a handler if the call fails.
an optional part, which makes it pretty worthless, if it were not so already.
I end up working on a lot of code that uses Glib, so I tend to use g_strdup_printf() a lot, which works the same as asprintf().
Ultimately the cost of allocations is usually not a big deal, and you gain a lot of safety. Sure, you then have to remember to free(), but I'll take a memory leak over a segfault (and its possible security consequences) any day.
And if allocation cost is a problem, you can always go back and optimize with static buffers later. That shouldn't be the default that people reach for, though.
while (*t++ = *s++)
;
Honestly the elegance of this thing was one of the hooks that made me fall in love with C. But this was from a now-forgotten age of innocence, as there are so many "nopes" around this line-and-a-half that one would, rightly, be tarred and feathered for ever putting it in a program today.You're assigning a char to another, relying on the return value being 0 to detect end of string.
You're performing the copy while also increasing the pointers with ++ in the same expression.
You're using the cryptic ; empty statement to signify nop, thereby confusing newbies.
Etc
You can printf non-null terminated strings too. Check printf("%.*s", length, strptr).
I haven't checked yet, but I'm about 90% confident that's UB. Is printf() guaranteed not to read to the end of the string when you give it a length?
> Characters from the array are written up to (but not including) the terminating null character. If the precision is specified, no more than that many bytes are written. If the precision is not specified or is greater than the size of the array, the array shall contain a null character.
And this is a standard practice in libraries for printing pre-determined lengthed string.
https://learn.microsoft.com/en-us/previous-versions/windows/...
Since the May 2019 update, Windows 10 has supported declaring the code page in a manifest file.
In Visual Studio, you must add "/utf-8" to the compiler command line, this makes it parse the source code as a UTF-8 file, and makes it output UTF-8 string literals.
To make console output work, call the Win32 function "SetConsoleOutputCP(65001);"
To get support for opening files with names that aren't in your system codepage:
* Create a manifest file as shown in https://learn.microsoft.com/en-us/windows/apps/design /globalizing/use-utf8-code-page
* Add this as an "Additional Manifest File" in Visual Studio project settings for the manifest tool
Additionally, there is an undocumented NTDLL function "RtlInitNlsTables" that sets the code page for the process. It is difficult to use without a lot of example code, but some app locale type tools (used to change locale for a process) make use of this function.
I don't know how to type japanese on my phone, but the first is literally "early" surrounded by honorifics. The characters for konnichiwa means "this/now", "day" and the "wa" at the end is an article making the previous phrase the subject of the sentence. Same with konbanwa, but for evening instead of day.
no idea on the etymology of moshimoshi for answering the phone, though.
https://man.openbsd.org/strlcpy.3
https://man.openbsd.org/wcslcpy.3
which to me will help with some of these issues. Too bad other Operating Systems do not have these. On Linux there is libbsd to get these, but I would like to see these to be added to the stdc.
Instead the c23 standard is messing with realloc(3) which could break some old programs. I have not looked at that in detail yet, so maybe it is a non-issue :)
However, there is a problem that has nothing to do with old code: they increased the number of situations that constitute undefined behavior, with no public discussion and no justification. It's frankly dangerous behavior.
strlcpy is not so nice due to the strange (IMO) return value of the number of characters in the source string. Which could be the number of characters copied or much, much larger than the number of characters copied. snprintf does the same thing.
So using strlcpy is safe (by C's low bar) but using the return value may be highly unsafe.
IMHO I would like it much more if the return values were:
0: string copied
1: string partially copied but truncated
-1: Error, errno set. This can occur when src or dst are NULL.That’s not a string.
This is the upside of strlcpy. Whatever is in your output buffer is guaranteed to be a NUL terminated and have your desired length. strncpy does not make that guarantee. strcpy will give you something with a NUL terminator but it could be well past the end of the output buffer. Hello, CVE.
The more I write here, the more I realize how silly it would be to write anything dealing with human-readable text in C in 2023. I had been working on a C webserver a while back but I think I'm going to purge that from my local git server and start over with something else.
char *strcpy(char *dest, const char *src);
char *strncpy(char *dest, const char *src, size_t n);
size_t strlcpy(char *dest, const char *src, size_t size);
From that, how do you know that strncpy expects and produces a character array, strcpy expects and produces a string, and strlcpy takes either a character array or string and produces a string?Your descriptions of the string/character copy functions are factual and accurate. But correct use depends on programmer understanding. You do not get any runtime guarantee. And IME, when you are under a deadline, 13 function calls and 3 message queues deep in some ancient codebase, while trying to get a non-trivial feature working, the distinction between a character array and a string is easily forgotten.
Anyways, my assertions are:
1) Using C for strings is a poor choice 2) If your compiler vendor or cranky boss forces use of C, use strlcpy to handle string copying.
And apparently it all started with some genius deciding that using a single 0-byte at the end is so deliciously efficient and therefore obviously the way to go. We can't waste 4 bytes for the string length, that's out of the question. I think only the Pascal solution of having a single byte for the string length is worse.
But you might be asking. “Why can’t I just assign the source variable directly to the destination variable?”
int main() {
char source[] = "Hello, world!";
char* destination = source;
strcpy(destination, source); // Copy the source string to the destination string
printf("Source: %s\n", source);
printf("Destination: %s\n", destination);
return 0;
}
You can. It’s just that destination now becomes a char* and exists as a pointer to the source character array. If that isn’t what you want them this will almost certainly cause issues.In some contexts, the name of an array decays to a pointer to its first element. That is a better way of putting it, and it's a (much) weaker statement.
Edit: if they were the same, this code:
int foo[] = {1, 2, 3};
int *bar = foo;
printf("%zu and %zu\n", sizeof foo, sizeof bar);
Would print the same valde twice, but it doesn't. On Ideone [1] I got 12 and 8. int a[] = {1, 2, 3}
int (*p1)[3] = &a; // ok
int (*p2)[3] = &a[0]; // not ok
int *p3 = &a; // not ok
(It should be noted that these will compile with warnings in C due to implicit conversions via void*, but you're still risking UB if you actually use the resulting value. They are all errors in C++ because it doesn't have implicit conversion from void*.)In any case, strcpy_s (which is a good replacement for strcpy) is part of the C11 standard. I'm confused how that isn't considered portable.
The idea may not be perfect, but for C which is intended to be low overhead, strcpy_s is about as good as it gets. If you want something more user friendly, that is what C++ is for with std::string, or library implementations like Boost or QT string.
Source, and why does it matter who made it?
>designed by some intern
You don't know that, nor is that how standards work.
>other compilers may or may not
So you've said basically nothing.
>it is worthless in any case.
It offers a low-overhead, safer alternative to strcpy. Is it perfect? No. But it's one of the better C options for those limited to the standard library.
Well, I mean, the docs specifically call that out: "If src is less than len characters long, the remainder of dst is filled with ‘\0’ characters. Otherwise, dst is not terminated."
So if you want null termination in all cases, you need to pass len-1, not len.
There are legitimate reasons why someone would rather write C, but "I don't understand RAII" is not one of them.
There’s nothing wrong with “C with classes and strings” idea by itself, if that is your choice or a consciously sufficient level of competence.
Which was exerbated by an obsolete c compiler that only supported c89.
https://www.pixelbeat.org/programming/gcc/string_buffers.htm...
RE counting characters: this is a whole can of worms. Do you want to count grapheme clusters? Code points? Anything other than just the amount of bytes? Use a unicode library.
The latter part of this article is a bit like those articles that make fun of javascript for having floating point numbers behave like, gasp, floating point numbers.
The 'n' in strncpy is mainly there to help you avoid overrunning the destination, it does not guarantee whatever makes in there is null-terminated.
This is why you should always explicitly set the last byte to zero after using strncpy (and never ever use strcpy).
char dest[16];
strncpy(dest, src, 15);
dest[15]=0;https://beej.us/guide/bgc/html/split/unicode-wide-characters...
This does mean you have to write a lot of C code for what would be simple tasks in other languages, e.g. a correct file open, read-to-dynamically-allocated-memory, and file close with good error checking is a full page (at least) of dense code in C and just two lines in Python.
If you've done a good job sanitizing and verifying all the input to your program, only then does it becomes relatively safe to use the standard string functions, with caveats for multithreading.
Asking ChatGPT to compare and contrast fgetc and fgets is a good place to start, and then ask how to use fgets to handle errors during stream I/O, and what can go wrong with multithreading etc. Then take a look at the sqlite source code for in-house C-string handling, here's the take-away comment:
"Because there is no consistency, we will define our own."
strdup:
> The strdup() function returns a pointer to a new string which is a duplicate of the string s. Memory for the new string is obtained with malloc(3), and can be freed with free(3).
Surely this is not the case for Japanese versions of Windows (or users with Japanese set as their display language?)
0 Lucida Console
00 Consolas
932 *MS ゴシック [MS Gothic for Japanese]
936 *新宋体 [Simsun for simplified Chinese]
949 *굴림체 [Gulimche for Korean]
950 *細明體 [Windows MingLiU for traditional Chinese]
Note that there are actually two global defaults which only are differentiated by leading zeros. This is intentional and can be used to enable additional fonts; it is a common tweak for Korean (and probably other CJK) users to add a preferred font with a name 0949 or 00949 etc.ITYM “equal”, not “true”.
> The strcmp() and strncmp() functions return an integer greater than, equal to, or less than 0, according to whether the string s1 is greater than, equal to, or less than the string s2. The comparison is done using unsigned characters, so that ‘\200’ is greater than ‘\0’.