What Is Systems Programming, Really?
willcrichton.net
willcrichton.net
I find that distinction to be a bit arbituary. To me, systems programming is one which require adherence to some sort of well defined spec/standard so as to interoperate with some (potentially underlying) "system".
In a 3D engine, the systems programming part is the part that interfaces with the graphics drivers, but not the part that derives the 3D geometry from reading files or running logic to generate the content.
In a DNS, the systems programming part is the socket interface, but not the DNS protocol (tho you could argue that the protocol is also considered "system-ish").
In a web application, the systems programming is also the socket, in a very similar vein to DNS servers.
In a desktop application, the systems programming is the interface into any native resources (such as any socket calls, any disk, or peripherals, as well as any of the graphical hardware interfacing required).
But because many of these tasks are quite common, they get put into libraries, and so many application programmers do not deal with these "systems" part, and just deal with their own application logic, and so it feels like there's no systems programming.
You have the kernel-level systems programming where you need to be near the bare metal, where you write kernels or program microcontrollers with less RAM than a x86 CPU has L1 cache, where taking a microsecond longer can mean the overall system crashing (or even costing a human life).
And you have system-level systems programming where you write services and supportive infrastructure to enable user programs, where garbage collection and non-realtime behaviour is acceptable, where you can skip a microsecond and it'll be alright.
The plumbing below, ie graphics rendering pipeline, tend to fall into the category of kernel systems programming, controlling the GPU is pretty close to doing what a kernel does IMO.
System systems programming is what it says on the tin; it's about constructing systems, multiple objects interacting with rules on the interaction, etc. An alternative name might be "user space systems programming" though again I think that doesn't convey the meaning as well.
Having a user interface doesn't really matter IMO, otherwise none of the usermode systems programming applications would be systems programming at all.
Perhaps it would help to add that user software applications generally aren't intended to be used by other applications. Your browser is such an application since it's primary interface is being directly used by the user. The output and state of a browser is not intended to be consumed by other applications running on the system, though it is capable of running code to supplement the output and state of a single website for the purpose of further user interfacing.
An RDBMS doesn't have such a thing, the user interface is largely what you use in the application, it's primary use case is gluing together applications like browsers or websites.
You cannot either count on the hardware protection to make the distinction. Some systems may use the hardware to protect objects at a different level of granularity. For example, capability based OSes will use a single address space, and use the hardware to protect not processes, but smaller objects (capabilities). Accesses and communications is not managed to prevent processes to access objects of other processes, but to prevent or allow access to capabilities. In a softer way, it's also the case of Lisp OS (including eg. GNU emacs). There's not much specific protection on these systems, but it still works safely, because you can access only the objects to which you have a reference, and you cannot compute random addresses (it's a controlled execution environment).
Andrei didn't create D; he's been influential in it, especially w.r.t. D's metaprogramming story, but the language was created and is primarily maintained by Walter Bright.
D2 introduced a lot of new keywords, new semantics, not two "standard" libraries like with D1 where there were Tango. Tango is pretty much completely obsolete in D2. Most D1 code might work to an extend with the D2 compiler, BUT almost no D2 code will be able to run using a D1 compiler and it'll even be difficult to translate it directly.
D2 is basically when Andrei joined the development of D back in 2007.
It's a C-centric view of the world and I wonder what old school FORTRAN77 people or lispers would think of this.
Anyway, I think the right to do this should be a privilege of the compiler. As soon as you claim this right, you abandon all possible support she can give you in battling all kinds of silly mistakes.
"Higher-level" languages (like Lisp) can help you in other regards. They can include proper primitives (or give you the proper tools to write the proper primitives yourself) to ensure that direct-to-hardware access is safe, can give you better tools to manipulate DMA buffers (e.g. proper support for coroutines when manipulating double-buffered data is cool), can help you write more generic state-applying and state-reading code (i.e. code for "turn these bytes from this circular buffer into this structure" or "take this structure that represents a peripheral's current config and use it to write the proper magic numbers in the right registers"). They can offer you better semantics for translating human-readable(-ish) configuration into the right stream of bytes, or manipulate peripherals in real-time. They can help you write a better VM (or their runtime can outright be the better VM you need).
But at the end of the day, writing to the config space of a memory-mapped device is still going to consist of taking a number (or building it from several numbers) and writing bytes at that address, no matter what language you're using. Better runtime support for safe operation under this scenario, better semantics -- they're all important, but what the author expresses is not C-centric in any way.
How is "val = PEEK adr" and "POKE adr, val" any less polluting than allowing "val = &adr" and "&adr = val" in select places?
Hieroglyphs are easy to pass by.
void DoSomeUnsafeStuffHere()
{
// regular code here. pointers verboten.
unsafe
{
// pointery stuff here
}
}
I think that is equally clear, if not even more. And again: I don't think this is any more polluting than weird out of place PEEK/POKE statements.You cannot simply search for & and * regarding point operations because those are also valid numeric operators, thus with context dependent meaning.
Being able to search for PEEK/POKE, SYSTEM.PUT/SYSTEM.GET, or just unsafe blocks makes all the difference tracking down unsafe code.
Also note that unsafe code blocks is older than C almost for 10 years, as ESPOL/NEWP already had such feature in 1961.
Which is one of the reasons why in the modern world (of fast CPUs and large RAM) code should be edited using editors that "understand" the language's syntax.
The following is perfectly typesafe:
SuperClass[] items = GetArrayOfSubClass();
Now if we try to do unsafe things to items, we may end up accidentally accessing memory we shouldn't. unsafe
{
for (int i=0; i < items.Length; i++)
{
var currentItem = &items[i*sizeof(SuperClass)];
}
}
This may or may not work, based on the runtime memory-layout of the subclass.Basically the presence of unsafe {} means that beyond this point correctness cannot be guaranteed by the compiler. That's in fact what the keyword is for.
And that's the marker you are looking for. No need to go digging deeper into the code looking for the actual pointers themselves.
* Pointers are required mainly so you can do pointer swaps and be sure there is no unnecessary allocation (in simulations this often means being sure there's no 10-100GB allocation each timestep, which is where it really matters).
* Pointer math is unnecessary and only there because C has no/poor support of multidimensional arrays. Use a reasonable language like Julia or Fortran instead. In fact, the potential for pointer math has a big negative impact on performance since compilers have to assume aliasing. The __restrict solution is clunky in C and completely non-standard in C++, Fortran does not require this at all (no aliasing allowed except where explicitly stated).
* For almost all usecases (except swaps) a semi-managed approach like Fortran's allocatables or Objective-C's ARC or even retain/release seems to me the most straight-forward.
Many languages are getting around this issue by hiding pointers in their type system, performing copy-on-write, and taking the decision of whether allocations are performed on the stack or heap out of the hands of the programmer.
see, and that's where these languages become really clunky for HPC purposes. A compiler/runtime with HPC support (and I think also systems programming) should provide (a) performant defaults with safety as a second (but still high) priority and (b) the ability for programmers to go and set things a certain way when it is clear to them how things should be implemented on the machine.
Otherwise, in large applications, there's just too many moving parts that the compiler (optimization) can mess up. As long as we have no AGI baked into compilers it's an illusion to think that compilers will do the job of HPC engineers, so if you take away these tools we just have to look elsewhere.
Chapel is a strongly typed language designed from the ground up to replace C++ and Fortran for HPC programming in cluster environments, while removing the typical unsafe features from C and C++.
Initially designed by Cray, Intel has also given an helping hand.
My experience is only from language geek point of view, reading the papers.
Chapel Implementers and Users Workshop 2018 papers
sounds like Julia to me ...
> ... in cluster environments
this is where it may be differentiating itself, AFAIK Julia has not been very focused on clustering from start, but I'm assuming it's now getting better.
The issue I see with Chapel though is that HPC is such a small community compared to larger scientific computing, and Julia has such a good design that it is very easy to improve (using Julia code!) with a large developer group (currently 736 contributors, that is quite a bit more than CPython has ever had and Julia is a lot younger), Julia is a lot more nimble. So when Chapel gets a good idea, sure enough there's an MIT programming languages or HPC fellow who proposes it in Julia, and so you get stuff like https://github.com/JuliaLang/julia/pull/22631 soon after. That doesn't mean Chapel is bad, but Julia already has the largest contributor base of any of the open source scientific computing languages still being developed, and that manpower is definitely useful for keeping up with the current research trends (working on Julia is a good way to get a math/CS PhD at MIT :) )
In any case, the more the merrier. :)
I don’t know. Seems like a lot of kernels do extra things so driver writers can write fairly normal looking C and produce fairly safe drivers without just arbitrarily assigning pointers addresses from numbers. And even if you use C as your comparison, those helpers will often be written with some assembly language (read C isn’t completely good enough for the task all by itself) I don’t know of it existing but a kernel in go with ref counting GC and a handful of cgo primitives seems very possible and maybe even delightful to work on.
I would agree though that the language ecosystem is shifting a lot in the last few years. IMHO there are now a few languages that are becoming proper full stack languages in the sense that they scale from embedded all the way to browser development. Rust is a great example.
I'm really impressed with what the Rust community has done in just a few short years. All my life, C/C++ was the only game in town for the stuff that is now done with Rust with arguably the same or in some cases slightly better performance.
Stuff like WASM allows the use of system programming languages in places where they would not be used in the past. Using Rust transpiled to WASM makes it a proper fullstack language. And people are using it for that. And people do server development as well in it as well as OS development.
I think you're being a bit optimistic here: traditionally, the only language that has been able to do this is C++, and Rust is displacing it simply because Rust really tries to target C++ developers and their pains. But other than that I don't see much else development here. Swift has promise, but unfortunately nobody's really writing much framework or foundational code with it; at least not yet.
I'd say Object Pascal is able do it too, though it's certainly true that Pascal is not as widely used as C++. Free Pascal and Delphi are the two major Pascal implementations today:
But I agree, languages like Swift and Kotlin are a bit more logical choice as fullstack languages. Both of those are also extending their reach. Particulary, Kotlin code is a very nice upgrade from javascript in terms of expressiveness, tooling, and safety. I'm using it on top of the JVM currently. People are doing Android with it and the native compiler currently under development is explicitly targeted at doing native IOS and Android apps (i.e. not the current java based DEX vm common on Android).
eg when I extended the GINO-F drivers for the top of the line HP plotter that's systems programming but the GUI system I built to analyse soil samples was not - even though they where both written in the same language
At first I thought it was poking fun; now I see it as a comment about what is relevant at a given level of abstraction.
When discussing the suitability of different programming languages I always point to the problem at hand and identifying the abstraction level the problem is at. What your problem requires you to care about and pay attention to gives this.
Ideally, solving your problem would be a one-liner in an already existing DSL created for your specific problem (domain). In reality, this rarely happens and you must have your pick beteeen everything from niche DSL:s to assembly, but the key really is identifying that problem abstraction level.
I work in a mainly C/JS shop whereas I privately prefer Rust/ReasonML. Rust would be a great fit at work, but Go would also work nicely for tons of things we do. Alas, the inertia of organizations.
I like thinking about the practice of programming as creating a DSL in a language to solve your specific problem.
My view of Systems Programming will forever be colored by my start with IBM mainframes. The Systems Programmer maintained all the utilities that the mainframe provided, and also the frameworks that supported online programming.
I suppose today's equivalent is a combination of a sysadmin and an applications developer, providing the applications are things like container components, build pipelines, etc.
A language is disqualified if it requires an OS or if it requires code written in a different non-assembly language. Cheating, by adding that as a huge (impractical) amount of assembly, also disqualifies a language.
Funny story about gcc: The compiler demands a memcpy, which it sometimes uses for struct assignment. If you implement memcpy in the normal way, gcc will helpfully replace your implementation with a call to memcpy! Despite this, C is still a systems programming language.
There is some stuff missing in your list: preparing the stack pointer, address layout, etc. You also need tools to produce text and data sections that can be loaded at a specific address. Even if C is a low level language, there is still quite some stuff between it and the "bare hardware". In that sense, the only language that gives you enough control to produce the binary exactly in the form needed by the hardware (without relying on external tools or libraries) is assembly.
Though, of course, most compilers provide extensions that make this task generally possible within C (using the loose definition of "I don't need any separate files or inline assembly, just attributes and flags).
Leaving out libraries is normal for a systems programming language. It is fine unless the language entirely doesn't work without the libraries.
C is not cheating, the compiler assumes you work on user level stuff. Want to write bare metal (kernel) then you need to tell the compiler.
Strictly speaking that isn't proper C as defined by ISO/IEC 9899:2018.
But I disagree with this:
> Companies like Dropbox were able to build surprisingly large and scalable systems on just Python.
Many of Dropbox’s services are written in Go, and Magic Pocket is written (partly, at least) in Rust. Earlier in their development, Dropbox relied on S3, which is obviously not in Python.
Fundamentally, I think the important part of the “production” aspect of “systems programming” is that it’ll be used a lot. That’s what drives the requirement for efficiency: if you’re 10-100x less efficient, someone else will be more cost effective / solve bigger problems.
As an additional example, GitHub was written in Ruby, but that’s fine because the underlying Git and filesystem manipulation is all in C. The same thing is sort of true of Facebook’s world of PHP: PHP is mostly a wrapper around C libraries. Until it got complex enough that they rewrote the language.
tl;dr: I don’t think systems programming needs to be “low level”, but production systems do need to be efficient. More powerful computing just moves that further to the right!
And since I sense the question coming, Android, OS X/iOS, ChromeOS might use an UNIXy kernel, but its exposure surface to applications is so small, that it can be easily exchange for something else, it is just a matter of cost.
Which brings us to the next point, given the commodity of free UNIX-like OSes, maybe only an hardware reboot like Quantum computers will actually do it.
Android, macOS, and iOS expose almost the full set of POSIX to the programmer. Now, you may argue that it goes mostly unused, but it is there, and there are people using it.
Using libc is not the same as POSIX.
https://developer.android.com/ndk/guides/stable_apis
https://developer.android.com/about/versions/nougat/android-...
https://android-developers.googleblog.com/2016/06/android-ch...
As for macOS, and iOS, if Apple removed POSIX how many apps written in Objective-C and Swift (not UNIX cli tools ported from Linux) would actually be affected?
Very few, because those written in C and C++ ported from other platforms are most likely making use of ANSI C and C++ standards.
I gave my take here:
https://lobste.rs/s/jrzwgy/what_is_systems_programming_reall...
In short, I think shell is actually a more appropriate language for describing systems, rather than OCaml or Haskell! If that sounds weird, then I'll point you to the evidence of shell already being used for this purpose (e.g. the 40K lines of shell in the Kubernetes repo I mentioned).
Although, on reading the blog post a little more closely, we may not be agreeing precisely on the problem.
I should probably write my own blog post about this ... there are a couple I linked that give something a flavor.
Bugs in application code causes errors in that application.
Bugs in system code causes errors in an user application.
If bugs in your code causes problems only to your code - it's single application. If bugs in your code causes problems in other programs - it's system of programs.