daivox wrote:
IIRC, POSIX relies on C, and C is simply not something that runs quite optimally on 65xx CPUs, no?
No, POSIX is an API standard, specifying a Unix-like API for operating systems. It says nothing about
how it's implemented, although the specification does use C for detailing various data structures.
Lunix (for the Commodore 64/128 computers) implements an API that is clearly inspired by POSIX, for example, even if it isn't completely POSIX. This OS is written in a combination of assembly language and cc65 (with the kernel written in assembly, and most of the on-disk utilities written in C).
Quote:
My plans are grand, however. It will be completely self-hosting, with the same format for executable files AND for libraries, meaning not only that the linker doesn't have to think about two different file formats internally, but also that a program can double as a library!
Nothing grand about that -- what I want to know is, what is taking people so long about this? Python, for example, implements this philosophy. I use this all the time at work:
Code:
#!/usr/bin/python
import foo
import bar
def AGlobalFunction(...):
...
class AGlobalClass(foo.ABaseClass):
...
def main(args):
...
if __name__=='__main__':
main(sys.argv)
This allows me to import an otherwise "executable" Python script as a module elsewhere, while still retaining the ability to directly execute it if it makes sense to.
The other nice thing with this is the fact that a compiler can
compile directly to an executable without concern for linking of any kind. The loader
is the linker. The only need for an external linker is for creating JAR-like entities, where you combine multiple modules into a single meta-module.
Quote:
The biggest problem I've had is that I haven't written the toolchain yet, because I'm a freakin' amateur and am learning everything by doing.
From someone who already went down this path:
*
Start Backwards.
Solving a puzzle is always easiest if you work your way through it backwards. For example, ignore the language issues, and jump directly to the loader. Make something that works with hand-crafted, assembly language files made to look like binaries produced by a compiler as input. This way, you have your input under full control as you debug the loader.
*
Just Let Go or
You Aren't Gonna Need It
Don't over-engineer your code. Don't under-engineer it either. Every time you say, "Shoot, I'm going to need XYZ," just walk away. Solve the problem you have
now and only that problem. Who cares if it isn't the right solution? You can always fix it later.
*
Throw it away; you will anyhow.
Part of "fixing it later" is realizing that code is cheap -- it is the
interfaces between code that is more expensive. Therefore, if you realize that it'd be easier to just start a chunk of code over from scratch than retrofitting what's already there,
do it. As long as the new code conforms to whatever interface(s) are relavent, nobody will be the wiser.
By way of analogy, a Mazda RX-7 runs using a Wankel rotary engine. These engines operate very differently from a reciprocating engine. However, if you find you want more low-end torque from the '7, you have no choice but to either put in a larger Wankel, or replace it outright with a Ford 302 V8 engine. The fit isn't perfect in either case, and adapters need to be implemented, but many people have done this with satisfying results. The point being, it's not the specific details of the engine that matter, but rather, the
interface between the engine and the transmission.
Indeed, conversion of a car to full electric relies on the same principle.
This works with software too. It's what allows my applications to be written in Forth, in C, or in Haskell, and Linux doesn't give a hoot what I choose. As long as my code makes use of the Linux system call interface, it's happy, and I'm happy. Take advantage of this concept in your code too.
A corollary to this is to prototype everything, and with very rare exceptions,
never use prototype code in production code. You'll find the quality of your code is better the second time around anyway.
Sometimes, it is the interface itself which needs to be adjusted. That's OK too, as long as you're careful! Remember that changing an interface directly implies that
two or more pieces of code will need to be altered as well, since an interface always has a provider and at least one consumer. Which brings me to...
*
Test, test, test, test, test, test, and test some more.
Test Driven Development. I cannot speak highly enough about this topic. Write your unit tests
first. Then you write your production code to satisfy the unit tests. This has several highly desirable attributes:
1. Well written unit tests can be written in
any order whatsoever. This means that you can test high-level code long before you test low-level code, or vice versa, or even a mish-mash. It achieves this because . . .
2. Unit tests
enforce natural boundaries of modularity. If you find you can't test malloc() because it depends on the memory pool implementation somehow, you either need to find a way to guarantee memory pool accessors are tested first,
or, preferred, write a mock set of memory pool accessors just for the purpose of testing.
3. Unit tests are automated. As you implement more and more functionality in the production code, you'll find that you'll periodically break another unit test accidentally (e.g., an interface was inadvertently changed).
Fix the broken test or the production code under test before implementing the next feature.
4. Releases can be made only with 100% unit test success rate. If even one test fails, slip your schedule and fix it. Trust me on this.
5. Concentrate on the highest priority features first. Unix is successful because of the 80/20 rule -- the API is
far from perfect, but it does solve 80% of the programming problems elegantly. The remaining 20% can be made up for in other ways. It's better to have something that works Kinda OK, than to have a perfectly non-working system. This is the "Worse is Better" approach to code design. I'm not happy with it, personally, but there's no arguing with the results.
Unit tests aren't perfect of course. Being software, they can be just as buggy as the production code you're coding. You'll find that coding will take 2x to 3x as long as you would with a debugger. However, in my decades of coding experience, I've found that using TDD eliminates all but the most dire need for a debugger all-together, that well-written unit tests serve as documentation for how to use various interfaces, mocking dependencies show how pieces of code
fit together in the big picture (very important as systems get complex!), etc.
But that's the nice thing about unit tests. It's not the correctness of any one piece of code that you're looking for, but rather,
agreement between the test and the production code. The reason is simple: given an incorrect assumption about the specification of a program, you can write just as incorrect code using formal analysis as you can with unit tests. So, don't worry about unit tests being buggy.
Discrepencies between production and test code will be much, much more instructive than correctness.
By way of analogy, notice that all modern, complex electronics have some form of testing functionality built-in, be it with JTAG interfaces or some other system. There is no reason complex software should be an exception.