Why I use C for Flight Software
This post is about why I use C for any serious project at work, and why I would choose to use C again even if starting from stratch on a new project. I am hoping to cover where this choice comes from and what advantages and disadvantages it has, specifically for the sort of work that I do. Its not intended to be a rant or to put down other languages or processes- I just want this blog to provide some insight into my experiences with what flight software is like at NASA Langley Research Center.
This choice is certainly not motivated by something as simple as performance- its the result of the particular tradeoffs that apply to flight software. All engineering choices have tradeoffs, and have to be understood in terms of the costs that apply to a particular situation. The most important things for flight software, in my experience, are: simplicity, maintanability, consistency, testability, and reviewability. These may not be surprising, but the decisions that they motivate can be very different from the decisions I see in other software.
Language choice is a very important- you inherit your ecosystem, tools, and the mentality and capabilities of your language as your framework for expressing yourself, and its important to think carefully about language when starting a project. There is some tendency to obsess about languages, but if you are going to be using a tool for years on a large project, it does merit some thought.
I see places in this post where my thinking may be formed by my choice of tools, where my estimate of costs and benefits are from the perspective of the tools and practices that I have already chosen to use. I've certainly used other languages at work- Haskell, Ruby, Python, Perl, R, MatLab- and outside of work- C#, Lua, Rust, Java, and more. However, my serious development has all be in C/C++ and I can't help but see things in the light of my tools.
Most of these topics deserve posts of their own, but I wanted to at least get the ideas down first. It could also use more examples, but its become too long and just needs to be posted.
The Domain
The particular domain I am talking about here is embedded systems work for the flight software systems I have worked on at NASA. These are not the resource contrained systems that some embedded software inhabits- I haven't done microcontrollers for this work, I've only done processor boards with hundreds of megabytes of RAM and flash, and processors in the hundreds of megahertz range. They are fairly powerful for what they are- you need to be aware of resources, and careful about timing and latency, but they don't have the mentality of scarcity that you see in some systems.
In this domain, you interact with a number of other systems through some hardware interface, you accept commands, produce telemetry, and run one or more complex algorithm. Your code may never be in its "production" environment until it is actually in space (or whereever it goes), and if your code fails it can be catastrophic to the project. You don't have easy access to your production systems, and updating code- even for the simpliest change- could take monthes of development, review, testing, and operations time. In a sense, you need to get things right the first time, and your code needs to operate continually, perhaps for years at a time.
The Options
Its easy to rule out huge groups of languages- nothing with a garbage collector is deterministic enough, languages with a virtual machine don't supports all the processors and operating systems we use, and dynamic typing is just not an option when correctness is important.
In a way, the only options for flight software systems are C and C++. There are examples of other language like Python being used in cubesat projects, or assembly for some things. Certainly Ada is used in these domains, but I've never personally come across one of these systems.
The only contender that I see entering this space is Rust. All other alternatives I've seen never mature enough to be used in serious work, and Rust is not there yet. I'm hoping that one day it will prove itself, and become a contender in new projects, but we are not there yet.
Simplicity
In flight software, simplicity is vital. C is a relatively small language, and I prefer to only use a subset of the language. This means I use as few syntactic forms as possible, and enforce consistency in as many aspects of the code as possible. C could certainly be simplier, but it is at least possible to write very explicit C with enough displine. There are certainly dark corners of C, but compared to any other language I know, it has to be considered very simple.
Simplicity is required here because every language construct and every new construct interacts with every other one. Each thing that is allowed in the codebase introduces difficultly in reviewing and maintaining code- it is better to be safe and rely on only a small group of concepts then to risk some interaction causing a subtle failure. More abstract code can be smaller, which does aid review and maintanence, but for many of the problem we solve, C is at about the right level of abstraction, and we prefer explicit code to smaller, more indirect code.
Least Power
This is a case where additional power is often not helpful- its a principal of least power sort of situation. Most of the problems we solve are fairly straightforward- they do not usually require complex algorithms or data structures. For these situations, the cost of abstraction is very apparent. The systems are already complex- you have to be very careful where you add mental overhead through abstraction.
Consistency and Discipline
I think the consistency, simplicity, and determinism here would be considered draconian in other programming domains- we don't allocate memory after startup, we almost never free memory, we check the result of every function, we check every pointer for NULL, and we revalidate all inputs in every function, even if they are validated in calling functions. We declare all variables at the top of a function, we don't use single letter variables even in loops, we don't call functions in 'if' statements, we use consistent naming of all functions, variables, and globals. These practices are enforced to keep the code deterministic, to add in reviewing codebases, to ensure that errors are caught and do not propagate. Its very pessimistic programming, and each line of code that accomplish a task is followed by at least 5 lines of error handling, often more.
C++
This is a place where I see a real danger in C++. When I have used C++ I have restricted myself to a very small subset of the language, not using templates, inheritance, operator overloading, lambdas- we use it as an expanded C. The advantages you get are not bad- function overloading, default parameters, and access to a larger standard library do help. The cost that I see in this is that consistency becomes a much more difficult battle to fight. Its like you are standing on a precipice and you have to step carefully to avoid falling into a pit of complixity. It becomes a battle to keep things simple and choose which concepts to introduce, and it can become very easy to make mistakes. This is not an unjustified fear- I found a case within a complex algorithm where data was allocated at runtime, which is forbidden in our systems. It was not obvious that this was happening- the language left us open to an implicit allocation that we were not used to seeing or reviewing for.
Review
Flight software must all be reviewed, and code should be as transparent to the reviewers as possible. I don't like to see new concepts introduced unless they are really providing some benefit- the reviewers cognative overhead should be as small as you can make it. Reviewers have to catch a lot of possible mistakes in C- it is very easy to corrupt memory in particular- so discipline is required to avoid memsets, memcpys, and other functions which can cause corruption. Data structures and algorithm are kept as simple as possible.
This might seem like a case against C, but I find that it is very easy in other languages to introduce new concepts, and very hard to ensure that they do not lead to problems. In C++, as soon as I start to see a bunch of classes, my heart sinks because I know I'm going to have to jump through hoops to ensure that the code is correct. We almost never use inheritance for this reason- I don't want choices of what function is called to be determined by the runtime properties of code.
In a sense, the advantage to C here is its lack of means of abstraction- the way to build complex systems is through procedures. Lacking other mechanisms, you always know how some feature will be built- structs, unions, enums, #defines, and functions. When you review the code, you will not be learning some tower of abstraction, or looking through files trying to find where something is implemented, or testing your ability to remember some complex dispatch mechanism. Its just procedures and data all the way down.
The main place where you might not be able to follow the code precisely is when function pointers are used. We do use function pointers, but only in certain modules, and only in fairly simple ways.
Note that we don't use complex macros, which can lead to some very complex constructions.
Tooling
Part of tooling for me is LabWindows, VxWorks, CFE/CFS, and a flight software architecture used at Langley. It happens that I am in an environment that makes a great deal of use of C, so naturally it is more confortable to keep it that way. VxWorks is one example where C++ fits, but for the others, C is the native language.
Some advantages here include being able to port code between an embedded system and a LabWindows tool, and making it easier to support compiling an subset of the embedded code on a desktop. Certainly this can be done in C++, but I've always found it simplier to do with C then C++.
Another part of tooling is that the C language is simplier then C++, and easier to integrate with FFIs such as Lua or Python, and just simplier overall. This is an example of the downstream costs of complexity that effect every tool and concept involved in your code.
Nearly all FFIs seem to be for C, and support for C++ is partial and far more complex. Parsing C++ is hugely complex, its name mangling is a pain, and its size makes it unmanageble when a high degree of control is required.
Disadvantages
Safety
If I could express at least some of my code in a safer language, use more modern programming concepts, and have more algorithms and structures available, I would be more productive. There are many places where its easy to get bitten by the C/C++ scalpel, or to find that we can't be sure of correctness of a section of code without a great deal of work. We handle this with stringent practices, review, unit testing, and system testing, but I would prefer to not have to worry about some of the problems that lurk in most C/C++ code.
Something like ivory, Rust, or one of the other safer but still low level languages might be doable one day, and I would love to incorporate it into a smaller project and see if it helps.
However, we have to be pessimistic in our evaluation and only use languages and code that we trust in this domain. No research or untested code can be used in the large projects- we have to make the best decision as engineers who are following a process. We can't afford costly bugs or code that we don't understand fully, and we already have so much infrastructure and so many practices built around the C/C++ paradigm that its very hard to make any other choice. We have to estimate cost and schedule, and use operating systems, drivers, and board support packages written in C/C++. This is where Rust's ability to be integrated into and to talk to other code could be its critical feature to find its way into high assurance code.
Level of Abstraction
For some tasks, the level of abstraction in C is about right- moving data around, interacting with hardware, and doing simple tasks. Certainly I wish it were better with things like endianness and bitfields, but we work around its shortcomings. However, sometimes its limitations do become confining rather then freeing and I wish for a different level of abstraction.
I see places where using the same language for everything means that it is almost never at exactly the right level of abstraction. The language tower is a good example of where system architecture could be domain specific language, leaving implementation details to another language.
In some code, I would love to have a separate language, or subset of a language, where additional checking could be done, or which does not have ways of expressing unsafe code. This is another principal of least power situation- I would like the option to use less power when appropriate, but in C you have full power all the time.
The other place I see this is in complex algorithm The lack of abstraction and built-in tools can make this code more complex then necessary, and it is a place where translating from another language does make sense. This is done in some systems- I've heard of Simulink used this way- but even with this strategy you face issues with trusting the algorithm code. You have to make some decisions on how to review it, test it, and ensure maintain it as part of the rest of the system.
C++ certainly has a lot of power, and I could see it being a step in the right direction in some cases. My main relunctance for these algorithms would be wanting to avoid introducing too many new concepts- each new concept and its interaction with other concepts in the code adds complexity and overhead into a largely manual review and testing process. I could see some of this being aleviated by better tooling that could replace manual components, but I still think adheritance to simplicity is an important thing to hold on to.
Type Safety
C is not a type safe language. There are certain things it will catch, but in many cases the correct use of types is up to the programmer. It is not dynamic, but we can cast pointers freely, and the language does not help us avoid issues with memory use. We can cast incorrectly, index off of arrays, and overwrite memory almost anywhere.
One argument for C++ would be the ability to express more in its type system, and the ability to more clearly express and check casts. I'm not well versed in this style, so it is hard for me to evaluate how this relates to my work. There may be an intermediate where some safety can be gained with very little complexity that would improve overall code quality.
Discipline
Some of the advantges to developing in C come from strict discipline in development, rather then something provided by the language. I would love to have a language that would aleviate the manual work we do in reviews to make sure we do things safely and catch error cases. Again, in principal C++ can help with this, but I haven't been willing to accept the cost of increased complexity that it brings.
Conclusion
Look back, this post has as much about disadvantages as it has about advantages. I think what this tells me is that we live in a messy and imperfect world, where the tool that provides the best results comes with a host of disadvantages that we live with. If there were a language without the edge cases, the portability issues, and with simplier syntax and fewer choices, which caught more mistakes in safety and memory usage, I would use it, but it does not exist.
I wonder if using Ada could result in safer and better code, or some subset of C++ chosen for flight software. Or maybe Rust will take over everything. I don't know, but for now, when I'm writing code that has to be correct, I use C.