But that's not how C "rolls", and we'll never get that. So I guess we now have 41384 ways to do buffer overflow guards.
I agree that this will never be implemented as a standard, but I think that's a good thing. Higher level languages push against their boundaries non stop. Java has libraries and frameworks that fundamentally change the syntax and functionality of the language. C knows what it is. If you want something that it can't do it promises that you can either build it yourself or switch to a different tool.
All of this to say, C has a single suggested way of doing this: using a different language. That's part of why we built them
Note that C does have strong conventions, such as that strings are terminated by a zero byte. Nothing in the language demands that, it’s just a convention! C could adopt better conventions.
I'm really glad that C doesn't do this, personally. It would reduce one of the main advantages of the language.
Is there a library that you recommend for this?
1. Define a macro function for retrieving the length of an array:
#define LEN(arr) (sizeof (arr) / sizeof (arr)[0])
2. Don't introduce macro constants for array lengths; hard code the length in the declaration and use LEN to retrieve it. Example: int a[100];
...
for (i = 0; i < LEN(a); i++) {
...
}
3. Define a macro function for dynamic array allocation: #define NEW_ARRAY(ptr, n) \
(ptr) = malloc((n) * sizeof (ptr)[0]); \
if ((ptr) == NULL) { \
fprintf(stderr, "Memory allocation failed: %s\n", strerror(errno)); \
exit(EXIT_FAILURE); \
}
4. When you create a function with an array argument, also add an argument for the array length.5. Use a convention for naming the length of array pointer targets, for instance by adding the suffix `Len'. Example:
int *b, bLen = 100;
...
NEW_ARRAY(b, bLen); /* nice to know that b and bLen belong together */
...
SomeFunction(b, bLen, ...);
...
for (i = 0; i < bLen; i++) {
...
}
6. Define your own safe wrappers around unsafe standard library functions or use someone else's code that does that.There is an RFC proposal for the Clang frontend for adding bounds checking reminiscent of Microsoft's SAL. [2]
[1] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3003.pdf
[2] https://discourse.llvm.org/t/rfc-enforcing-bounds-safety-in-...
#define LEN(NAME) (sizeof NAME / sizeof(NAME)[0])
I think gcc has a warning for this pattern now: when the size of a pointer is divided by the size of its referent type.
More importantly, it has an odd extra level of indirection. The traditional definition is:
#define LEN(ARRAY) (sizeof ARRAY / sizeof (ARRAY)[0])
This means that to use LEN on an array, we have to take the address:
char *array[5];
LEN(&array); // -> 5
If we use LEN(array);
which is an easy mistake, we get: sizeof *array / sizeof (*array)[0]
which is sizeof (char *) / sizeof (char)
which is sizeof (char *)
which is likely 4 or 8.I do see that LEN is supposed to be (only) used in conjunction with ARR:
#define ARR(TYPE, NAME, COUNT) TYPE(*NAME)[COUNT]
but that isn't enforced. An idea would be to add some "secret" prefix or suffix to NAME like blah_ ## NAME, so that name cannot be referenced without going through the macros; i.e. if we define ARR(int, foo, 42) then there is no declared identifier foo; it actually declares blah_foo, and LEN(foo) knows about that, also adding the prefix. Thus mistakenly using LEN(foo) on something not declared with ARR will likely be a reference to an undeclared identifier. #define AT(NAME, IDX) \
((typeof(&(*NAME)[0])) \
((ASSERT(((size_t)IDX) * sizeof(*NAME)[0] < sizeof *NAME, \
"Buffer Overflow. Index [%lu] is out of range [0-%lu]", \
((size_t)IDX), ((sizeof *NAME / sizeof(*NAME)[0]) - 1))), \
((uchar *)*NAME) + ((size_t)IDX) * sizeof(*NAME)[0]))
Some of this might be pushed into non-inlined run-time
support function. That could be static and defined in the
header, to keep it header-only, but ideally there would be a .c
file so it's defined only once.When you factor in the definition of ASSERT, and the ERRLOG macro that is using, it's a lot of cruft for just one array access.
Some compile-time options (via preprocessor macros) to control the bloat would be useful; e.g. a way of compiling it so that AT will just predictably crash, without a detailed error message with __FILE__ and __LINE__ and all. Basically just the check, with a branch to some code that calls abort() if it's out of bounds.
Also doesn't cover all the string and memory buffer manipulations.
SAL and Frama-C are the bare minimum for security in C code.
It's a nice thought, don't get me wrong, but it's hard enough to convince people to add `-fsanitize=...` to their compiler flags. An entire separate static analysis tool with its own learning curve (and its own set of idiosyncrasies) doesn't really qualify for "bare minimum" IMO.
[1] https://learn.microsoft.com/en-us/cpp/code-quality/understan...
Modern C (2019) - https://news.ycombinator.com/item?id=36167820 - June 2023 (19 comments)
I try to avoid the malloc(n * sizeof (...)) pattern as much as possible. Sure there are lots of cases where it can never overflow, and you might save a bit of overhead from the zeroing and overflow checking, but most of that overhead might also be imaginary depending on allocator internals, and even kernel internals. It's the sort of thing it only makes sense to optimise when you've already squeezed out every bit of performance. And by then you've probably minimised dynamic allocation as much as possible anyway.
It's also very easy to think something like "well, n is passed in as a parameter, but it's a static function, and I know all the callers. So it's fine".
But now every caller in the future has to be aware of this possibility.
Can you clarify: what possibility should you be aware off with malloc that you don't need to be aware of with calloc?
size_t SomeIndex() {
static size_t example_index = 0;
return example_index++ % 2;
}
int main() {
NEW(int, arr, 1);
// This buffer overflow is not detected:
*AT(arr, SomeIndex()) = 42;
return 0;
}"'Oil,' says I, 'never explodes. It's the gas that forms that explodes.' But I shakes hands with him, anyway.
...
"'Listen,' says I. 'I instruct her to keep her lamp clean and well filled. If she does that it can't burst. And with the sand in it she knows it can't, and she don't worry.
— O. Henry, The Man Higher Up
EDIT: although, it seems like this looses much of its power once you start passing these buffers around to functions that do not use these macros.
Here is my oob.c program. I will show the output, and then the content of "oob.h".
#include <stdlib.h>
#include <stdio.h>
#include "oob.h"
int oob_fail(const char *file, int line)
{
fprintf(stderr, "%s:%d:out of bounds array access\n", file, line);
abort();
}
/*
* Declare properties of array type x
*/
#define ARRAY_ELTYPE_x int /* element type is int */
#define ARRAY_SIZE_x 7 /* number of elements is 7 */
/*
* Ensure array type x is fully declared at file scope
*/
ARRAY_FULLTYPE(x);
/*
* Inform the OOB module that the identifiers p and a are
* used as variables related to type x: either pointers
* to it or values.
*/
#define ARRAY_TYPEOF_p x
#define ARRAY_TYPEOF_a x
int get_elem(ARRAY_TYPE(x) *p, int i)
{
return APREF(p, i);
}
int main(void)
{
ARRAY_TYPE(x) a = ARRAY_INIT(1, 2, 3);
for (size_t i = 0; i <= ARRAY_SIZEOF(a); i++)
printf("a[%zd] == %d\n", i, get_elem(&a, i));
return 0;
}
Output: $ ./oob
a[0] == 1
a[1] == 2
a[2] == 3
a[3] == 0
a[4] == 0
a[5] == 0
a[6] == 0
oob.c:31:out of bounds array access
Aborted (core dumped)
The content of "oob.h" #ifndef OOB_H_435E_FDE9
#define OOB_H_435E_FDE9
int oob_fail(const char *file, int line);
#define OOB_PREFIX oob_ident_
#define OOB_XCAT(X, Y) X ## Y
#define OOB_CAT(X, Y) OOB_XCAT(X, Y)
#define ARRAY_ELTYPE(T) OOB_CAT(ARRAY_ELTYPE_, T)
#define ARRAY_SIZE(T) OOB_CAT(ARRAY_SIZE_, T)
#define ARRAY_TAG(T) OOB_CAT(ARRAY_TAG_, T)
#define ARRAY_FULLTYPE(T) \
struct ARRAY_TAG(T) { \
ARRAY_ELTYPE(T) a[ARRAY_SIZE(T)]; \
}
#define ARRAY_TYPE(T) struct ARRAY_TAG(T)
#define ARRAY_TYPEOF(V) OOB_CAT(ARRAY_TYPEOF_, V)
#define ARRAY_SIZEOF(V) ARRAY_SIZE(ARRAY_TYPEOF(V))
#define ARRAY_INIT(...) { { __VA_ARGS__ } }
#define AREF(ARRAY, I) \
(((size_t) (I) >= ARRAY_SIZEOF(ARRAY)) \
? oob_fail(__FILE__, __LINE__), (ARRAY).a[0] \
: (ARRAY).a[I])
#define APREF(PARRAY, I) \
(((size_t) (I) >= ARRAY_SIZEOF(PARRAY)) \
? oob_fail(__FILE__, __LINE__), (PARRAY)->a[0] \
: (PARRAY)->a[I])
#endif
Preprocessor invoked on oob.c (snipped down to the relevant part after the run-time support function oob_fail): struct ARRAY_TAG_x { int a[7]; };
int get_elem(struct ARRAY_TAG_x *p, int i)
{
return (((size_t) (i) >= 7) ? oob_fail("oob.c", 31), (p)->a[0] : (p)->a[i]);
}
int main(void)
{
struct ARRAY_TAG_x a = { { 1, 2, 3 } };
for (size_t i = 0; i <= 7; i++)
printf("a[%zd] == %d\n", i, get_elem(&a, i));
return 0;
}
It's clean enough to be readable (except, of course, code dense with AREF or APREF calls will be a mess). Uses arrays wrapped in structs, so you can pass arrays by value.You have to make a list of your variables that are involved and write some #define lines for them.
Same for the array types.
In my experience it's most likely that a function will write past the bounds of a buffer that's been passed as an argument. In that case, make sure the size of array is always included as an argument as you said in 4.
Even worse, even if you specify the argument to be "of the type" array, it will actually still decay to a pointer. Basically, this macro will only work if you use it in the same function the array is defined.
You should either add overflow checking to the macro or even better just use the damn libc api and call calloc. Or if you really insist on avoiding zeroing overhead, there's reallocarray(NULL, ...) if you use a reasonably modern libc.
int (*data)[datalen];
This requires you to dereference it once to get an array, then dereference it a second time to get a value. The advantage is that the array value can be used the same as an normal array on the stack, including passing it to the array length macro you describe.C still wins by far when writing libraries that will be used by lots of other people. Doesn't matter what language they are using, they will be able to add in a library written in C very easily. However, C++ or Rust libraries, even with appropriate bindings for the target language, users of the library will need to bring in an entirely new compiler tool chain that may or may not exist on the target architecture. But the C tool chain will exist for that architecture and be robust.
For new side projects, pick what you want to use of course. But for existing codebases and projects that aspire to have maximum impact, I recommend fully considering tradeoffs instead of thinking in terms of "clear winners".
C++ keeps C's crap array type as its native array type. You need to reach into the C++ standard library to get this awkward library type, std::array<type,N> and then finally you get an array type that remembers how big it is and has some basic features like swap.
Microsoft security team is on the record that just because they are adopting Rust, they won't shy away from C++.
Unless you mean array of anything like in typeless dynamic languages I do not see anything awkward about STL arrays in C++.
It makes writing (and hiring) a low-level project in C++ a much more complex task. It may have benefits, it may not. But C++ is so huge that it's difficult to judge whether it would offer an advantage.
And then there's the minefield of tooling in embedded development...
As a C++ developer, that sounds strange. Can you point me to some documentation about "dependent types"?
that being said, some do not want more macros either
Can be turned off on demand for relevant symbols.
> larger gap between source code and ISA
There's already a huge gap between C code and machine code (see: Undefined Behavior). C hasn't been a "portable assembler" for a very long time.
> impedance mismatch when working with C APIs
C++ has no problem working with C APIs.
When passing an array it decays into a pointer and the size is lost. We can also change sizeof to recover it, but there was a proposal for a _Lengthof operator which could work here.
Downside being, obviously, that it will only work with arrays of that particular length.
Alas it's even worse: once you pass buffers around to functions, you can't use these macros!
https://www.eff.org/deeplinks/2023/05/eus-proposed-cyber-res...
Except it's being used for serious development today
> going down the route of Java, i.e a corporate developed language with offshoots, which will end up with Rust being in the same crappy state.
So one of the most widely used applications programming languages in the world?
No, its being used for pet projects by people. Serious development = major companies using it in backends.
>So one of the most widely used applications programming languages in the world?
Because of CS programs, and legacy software written in java. Java has a community dedicated to pushing theoretical CS concepts into the language (much like Rust), while allowing things like a logging library to fetch code from anywhere on the internet and execute it, by default (which I would bet on would be the future of Rust given current trajectory)
You mean companies like Dropbox, Cloudflare, Amazon, Microsoft...? Are they too small to be relevant?
Example, Rust 1.0 had std::mem::uninitialized::<T>() which gives a T but it's obviously uninitialized. It's marked "unsafe" of course, but is that enough? Turns out they later realised that no, it's strictly never OK to do this, so the unsafe label was insufficiently cautious. Today std::mem::uninitialized is deprecated, Rust never removes stuff from the standard library, but you should not use this library call.
The type MaybeUninit<T> is the fix. Since MaybeUninit<T> might not be initialized, it's OK if it's not initialized, and since it might be T, it's OK for it to occupy the same amount of space as T. So, then we can initialize this memory, and tell the compiler it's initialized now, it's a T not a MaybeUninit<T>.
Can you guess how that works? It's pretty clever, and C++ could do almost the same trick, but it never has and my guess is it never will. If you don't know and are wondering, check that type definition carefully - MaybeUninit<T> is a union
For contrast, in his safety talk Bjarne Stroustrup just says as if it's obviously true, that it's safe to have uninitialized char arrays in C++. And his rationale sounds exactly like how std::mem::uninitialized happened - any possible value of a byte is a valid byte, so that's good enough, right? Nope, ask compiler engineers, there were plenty in the room when Bjarne said that, but he didn't ask them.
If we want to encourage Rust adoption, it is by having a middle path, not via Rust Advocacy Strike Force.
That only shuts the audience off, specially when Rust has a glass ceiling of depending on C++ infrastructure for its reference compilers.
And it's true that the rotten wood was better than nothing. Nobody is suggesting that NT or Linux should somehow have been developed in Rust in the 1990s. But likewise we shouldn't resist renewal in newer, better materials.
That applies to compiler internals too. Plenty of trouble down there for C++, it's just that C++ programmers can more often be sent away by assuring them that what they did was UB and so LLVM is entitled to miscompile it whereas the Rust people keep arriving with the receipts, in the form of LLVM IR that is lowered to machine code which makes no sense
And you might say, "Who cares? Even freestanding has the standard library". Nope, std::array wasn't added to freestanding. You can dig into the messy details for yourself if you want, but suffice to say your freestanding C++ doesn't have std::array
So the C++ language has "arrays" but they're garbage, and if you point out that the arrays are garbage you're told to use this library feature, which may not be available.
That doesn't make them garbage. That makes them annoying.
Which is fine if you write Windows desktop apps, but this is an array type, unlike a GUI widget, or an XML parser, it seems like I'd probably want an array type for this $1 per unit micro controller I'm writing firmware for. In Rust the nice array type works just fine, it's a proper first class type, it knows how big it is, mutable arrays coerce into a slice I can sort (only unstably, but hey, we're embedded firmware let's not get fancy), I can iterate over it properly... in C++ only the crappy C-style array is available unless I can butcher the std::array so that it works outside the hosted library. Ugh.
I have never perceived it as a problem. I do not think it really slows my programming. Personally I am the guy who would prefer function() vs fn() but without going into extremes of Java culture. Besides you can always alias it to whatever you want if your fingers are so sensitive.
Never been into this situation so from a practical standpoint it means zilch to me.
The problem with developers that don't do consulting is that they have no idea how each hour of their work relates to product development costs.
In Germany, services companies are already required to provide security fixes free of charge and warranties.
Someone has to pay those hours.
It is no accident that Google, Apple, Microsoft always mention increasing costs with bug fixes, when pushing for writing new code in safer languages.
Which tooling? Just curious, asking entirely in good faith. My recollection is that the majority of tooling I was using with C++ worked with Rust - debuggers, profilers, and sanitizers being the main tools. Although I find that I use them much less frequently since I don't find debuggers as useful for the types of bugs I have these days, and sanitizers are only useful if you have unsafe, and profilers are cool but usually I just write benchmarks using a crate and then iterate from there.
All the major C compilers are also C++ compilers, and none are (yet) Rust compilers, so out of the gate, C++ has similar availability to C.
https://overcast.fm/+LfVPHmBTo
Even if the tool chain exists, it must be adopted, unless you can rely on binaries being available for your end users, which will never be the case for a library which is just starting our. And adding another dependency to your build process, especially one as complex and with as many breaking version changes as C++, is a lot of work to take on.
If you are speaking to missing a rust compiler built on gcc, that seems to be an ongoing project with some momentum.
Realistically the most widely used architectures are now supported by rustc through llvm... x86, arm, riscv, and even to some extent xtensa now.
Power, arc, mips, sparc, and some others aren't too far away if someone cared enough.
If Linux can support Rust, I'd think that's a good sign most project can use Rust.
[0] - The Annotated C++ Reference Manual
Two years ago, your argument would have implied that Rust would never be allowed into the Linux kernel, and yet here we are.
But I see that you bring Rust in here. If that's your cup of tea then use it. No need to spill venom. Personally if I am dealing with $1 micros I very much prefer C with some selected libs for embedded. Do not really have problems with it for such small tasks.
If you use malloc(n * size), and n is too large, it could wrap around, malloc gets a smaller number than the program thinks it allocated. Which means that even if the program does bounds/null checking on the array later on, it has the wrong bounds. This can be used to access or modify other objects on the heap, or even modify allocator internals in some cases, depends on the implementation details of the allocator.
So what I meant was, you better be careful using malloc(n * size) unless n is a constant. If it's in any way tied to program behaviour or user input, it's a hole waiting to happen.
Neither of these may matter to you, but when they do, they really matter. So you still have to be thoughtful about using it. Not so different from how you have to be thoughtful about using malloc.
The thing I like about almost always allocating through calloc is this: I know that if my code is somehow not initialising memory properly, the resulting bug will be the same each time, and therefore faster to reproduce and debug. Not that I misinitialise my memory very frequently anymore, it's not that hard to get right.
Surprisingly often, I've found that so much of my data should probably default to zero anyway, so it doesn't really matter all that much.
Calloc can over-allocate, which i always found annoying myself, although at least with calloc, you know that if you only index the pointer modulo the n you passed onto calloc, you won't invoke any demons from the underworld.
But yeah, in general, to really know what you're doing in C, you kind of have to understand memory allocators at a fairly deep level, because the footguns are aplenty. You need to have a mental model of the heap and stack.
I never knew this was part of the standard. No documentation I saw for calloc (manpages, or similar) ever said it checked for overflow.
Of course, you can write your own malloc_array() that uses __builtin_mul_overflow() and doesn't come with calloc's drawback (the cost of zeroing the allocated memory).
And I believe you'll find it in glibc too these day? Or if not, there's always libbsd, which has lots of handy stuff anyways.
The Linux kernel already does extensive bespoke tooling and it's low level enough to skip cargo and such. It's rare to see that approach in Rust projects in the wild.
Same applies to the runtimes of the languages I use at work, and GPGPU related tooling when not using shaders.
Maybe then do a Go/zig/D, focus on cranelift and fully bootstrap Rust, before trying to rewrite the world.
Stated the same on HN earlier, but someone pointed out that literal strings are ASCIIZ.
If only. In C, it’s a (95+5)-item character set that happens to be a subset of ascii. See https://en.cppreference.com/w/c/language/charset:
“The basic literal character set consists of all characters of the basic character set, plus the following control characters”
That page also explicitly says:
The following characters are not in basic execution character set, but they are required to be encoded as a single byte in an ordinary character constant or ordinary string literal.
Code unit Character Glyph
U+0024 Dollar Sign $
U+0040 Commercial At @
U+0060 Grave Accent `”*
If I read that correctly, if you write a ‘$’ in a string literal before C23, there’s no guarantee that if gives you a byte with value 0x24.Of course, C++ is different. Like C, it makes a distinction between the encoding of source files (nowadays called the “basic character set”) and the encoding that the compiler converts literals to (nowadays called the “basic literal character set”), but it seems to put even fewer restrictions on them (in my cursory reading)
Also (https://en.cppreference.com/w/cpp/language/charset):
“Mapping from source file (other than a UTF-8 source file) (since C++23) characters to the basic character set (until C++23) translation character set (since C++23) during translation phase 1 is implementation-defined, so an implementation is required to document how the basic source characters are represented in source files.”*
If I understand that correctly, you can’t portably write an euro sign in C++ source files in C++ foe C++23
Also, chances are this changed in subtle ways between C and C++ versions.
11 'h' 'e' 'l' 'l' 'o' ' ' 'w' 'o' 'r' 'l' 'd' '\0'
ptr ^
C could be upgraded to do this in future versions, without too much backwards incompatibility.
"A string is a contiguous sequence of characters terminated by and including the first null character. .. The length of a string is the number of bytes preceding the null character"
This means, for example, strlen() must always check for the location of the first null character - there's no advantage to checking the length.
How would this work?
void *x = malloc(8);
...
uint64_t i = 5216694956355289088; // Python: int.from_bytes(b'Hello!\0\0')
memcpy(x, &i, 8);
char *s = x;
puts(s);
Assuming I did it correctly, this should print "Hello!".When the length get added to the start of the string?
But I'd hope that doing that would always be optional. There are numerous situations where that would seriously get in the way.
String literals are nul-terminated, e.g.: "foo"[3] == '\0'
Hint, someone else got it.
https://github.com/firecracker-microvm/firecracker/search?q=...
The fact that 'unsafe' even exists in Rust means it's no better than C with some macros.
Don't get me wrong, Rust has it's place, like all the other languages that came about for various reasons, but it's not going to gain wide adoption.
Future of programming consists of 2 languages - something like C that has a small instruction set for adopting to new hardware, and something that is very high level, higher than Python with LLM in the background. Everything in the middle is fodder.
Equating the existence of unsafe with C is laughable imho (it'd be barely comparable even if 100% of the rust code was in unsafe blocs, which never happens). Not even then it doesn't matter for the original point : rust is used in production for business critical functions, in large companies.
Parts of systems written in a language doesn't really mean anything for its adoption into mainstream. For example, Amazon uses Ruby heavily for a bunch of deployment stuff, but Ruby (sans Ruby on Rails that is in decline), is not really a mainstream language any more.
>Equating the existence of unsafe with C is laughable imho
Im not comparing it. The point is to demonstrate that unsafe exists for the sole reason of performance. In fast code you often want to directly access x[y] where x and y are variables, without having to run extra code around it. Its a well known computer science thing, as most of the code challenges given in interviews rely on this access pattern for optimal solutions.
And because of Rice theorem, a compiler cannot determine whether x[y] is always safe, as determining all the values y could take would involve running the program.
So as such, for all the advantages that Rust offers, you can have the same advantages with C with macros and LLVM extensions, albeit with less concise syntax.
https://www.microsoft.com/en-us/research/project/checked-c/
Similar arguments were used to justify Haskel about 6-7 years ago, and Haskel is pretty much dead in the water at this point.
The modern way to make a memory safe language is to focus on a high level language that doesn't require programmer to deal with memory directly, and then work on the compiler to make the resultant code optimal.
Second, extern "C" exists.
Third, in what concerns clang and MSVC, the C library is actually implemented in C++ with extern "C".
Even if it's the "same" toolchain for compiling C++ as it is C, adding the complexity of an additional language to the build process, and the extra versioning headaches that C++ adds over C, is enough to kill library adoption.
As I said originally, providing bindings is not the challenge, it's all the other stuff.
https://thephd.dev/to-save-c-we-must-save-abi-fixing-c-funct...
char buffer[] = "railroad";
char *s = buffer;
char *t = buffer + 4;
printf("mult: %ld\n", strlen(s) * strlen(t));
Suppose I read 100 bytes, formatted as "{name}\t{rank}\t{serial number}\t" using variable length parts.I can read the data into a single string buffer, replace the commas with NULs, and set up strings pointing to the middle of the buffer;
typedef struct {char buf[101], char *name, char *rank, char *serialno} person;
/* 100 bytes formatted as: name\trank\tserial no\t. */
int read_data(FILE *f, person *p) {
char *s;
if (fread(p->buf, 1, 100, f) != 100) return -1;
p->buf[100] = 0;
p->name = p->buf;
if ((s = strchr(p->buf, '\t') == NULL) return -2;
*s = 0;
p->rank = s+1;
if ((s = strchr(s+1, '\t') == NULL)) return -2;
*s = 0;
p->serialno = s+1;
if ((s = strchr(s+1, '\t') == NULL)) return -2;
*s = 0;
return 0;
}
person subject;
if (read_data(stdin, &subject)) fail("cannot read.");
print("Hello %s %s.\n", subject.rank, subject.name);
...
Even better, the protocol might have NUL characters already in the code, expecting C strings to point to the correct start.One of the things that makes C particularly suitable for certain sorts of tasks is that it's mostly WYSIWYG when it comes to the relationship between data structures and the actual memory layout. Having "hidden" things like a length value before the string steps on that.
char *s = "hello";
"hello" has length 6 because there's a hidden \0 even if I never wrote it in the code.Why did you say you "don't have time" then go to great lengths to not explain anything or back up what you're saying in any way?