Modulary for multiarch

When creating the FNK operating system, I found that one of the more unique challenges was to create a system in which different architectures could coexist. Take the memcpy function for example*:

void COMP_ATTR__WEAK__ common_memops_memcpy(void* dest, const void* src, size_t length) {
    uintptr_t* dw = dest;
    const uintptr_t* sw = src;
    
    for (; length >= sizeof(uintptr_t); length -= sizeof(uintptr_t))
        *(dw++) = *(sw++);

    for (uint8_t* db = (void*) dw, *sb = (void*) sw; length > 0; length--)
        *(db++) = *(sb++);
}

*Ignore the fact that it doesn't care about alignment... In a modern context it's kind of useless imo.
We would want to use this function in both the native (our executable format conversion tool is to run natively) and the target architecture (think embedded microprocessor—ARM).


Why is this a problem?

To see why this is a problem, lets look at how our build system would normally look like:

SOURCES := $(wildcard lib/*.c)
OBJFILES := $(patsubst %.c,%.o,$(SOURCES))
DEPFILES := $(patsubst %.c,%.d,$(SOURCES))
-include $(DEPFILES)

%.o: %.c
	$(CC) $(CFLAGS) -MMD -MP $(INCFLAGS) -c $< -o $@ $(CLINK)

For compiling something for one architecture it will work fine.

common
├── include
│   └── memops.h
├── lib
│   └── memops.c
└── Makefile

turns into

common
├── include
│   └── memops.h
├── lib
│   ├── memops.o
│   ├── memops.d
│   └── memops.c
└── Makefile

But say I want to compile this into native and nonnative .o files—now what? memops.o can't hold both architectures!

Fix 1

The first fix is to just attach the architecture name to every single file. You want to make something like this:

common
├── include
│   └── memops.h
├── lib
│   ├── *nativememops.o*
│   ├── *targetmemops.o*
│   ├── memops.d
│   └── memops.c
└── Makefile

Notice the native and target .o files The problem with this is just how annoying it is to write. The previous code we had written doesn't work. If we do something like

target%.o: %.c
	$(CC) $(CFLAGS) -MMD -MP $(INCFLAGS) -c $< -o $@ $(CLINK)
native%.o: %.c
	$(CCNATIVE) $(CNATIVEFLAGS) -MMD -MP $(NATIVEINCFLAGS) -c $< -o $@ $(CNATIVELINK)

the dependency files will not overlap and create something like nativememops.d. Trimming the name down is just annoying to say to the least.

Fix 2 (the good one)

It's a lot easier just to mirror the directory with mkdir -p $(dir $@) and then use the directory to do it. The file names are the exact same but they're much more organized. I ended up with a setup like this:

# Arch rules
$(OUTDIR)/%.o: %.c
	@mkdir -p $(dir $@)
	$(CC) $(CFLAGS) -MMD -MP $(INCFLAGS) -c $< -o $@ $(CLINK)

Because makefile has its builtin $(dir), you can do a lot of it the name manipulations easily.