strager / b-cc

Theoretically awesome build system, in C
0 stars 0 forks source link

Solve QuestionDispatch problem #27

Closed strager closed 9 years ago

strager commented 9 years ago

Snippets from my conversation with @evincarofautumn:

strager

So the problem I am trying to solve with b is this (if I recall correctly):

When the framework calls into your application saying, "hey, I have this Question and it is not Answered yet. Answer it."

There are three categories of things the application can do:

  1. Use some singleton implementation which shouldn't really change. Examples of this include environment variables, the host CPU, and the current working directory.

(Note that the implementation doesn't change; as in, getenv("FOO") should be implemented the same as getenv("BAR"). The code shouldn't really look at the contents of the Question and make decisions.)

(And by 'singleton' I mean 'the application developer shouldn't have to write the code'. There should be one obvious implementation that is already provided by a library.)

  1. Derive some Answer from some application-specific logic. For example, given a Question of "C compiler flags for target X", the Answer relies purely on application-provided code. The b framework doesn't know how to answer this Question since the Question isn't defined by the framework.
  2. A mix of 2. and 1. For example, if the Question is for a file like "libfoo.so", the application can either say "the user has a rule for libfoo.so, so I will run the linker" or it can say "the user has no rule for libfoo.so, so I will use the framework's default rule". The framework's default rule is to get a hash of the file's content if it exists, or raise an error if it doesn't.

"default rule" isn't really special here; the application could explicitly call b_file_question_existing_file_answer() or something.

So those are the constraints/use cases.

How I currently expose this to the application is through a "dispatch" process.

The application gives the framework a single function pointer. The function is called when any Question needs answering. The application inspects the Question's type and makes decisions based on the type.

So if the Question is of a type provided by the application, it will perform application-specific logic.

If the Question is of a type provided by the framework, but the behaviour needs to be changed (case 3 above), it will perform that changed behaviour, perhaps calling a fallback.

The problem with this approach is that it's not very modular. Adding a new type of Question requires modifying this function with a large switch table (effectively).

So, I thought perhaps I could take an OO approach and put the application-provided answering logic inside the Question-s themselves.

(This is accomplished by subclassing.)

The type check is gone, and everyone is happy.

But implementing subclassing in C is annoying and it seems to necessitate making my vtables dynamically allocated.

Why do I need the allocation? Example:

I have an event loop. The event loop has a handle. When I need to start a process (like gcc or clang), I register a callback in the event loop.

So if I have a FileQuestion subclass which compiles things, I need to stick the event loop handle in the FileQuestion vtable or in the FileQuestions themselves.

Actually, I don't need a heap allocation since I can just do a stack allocation. So it's not that bad.

I'm sure there's another way of solving this problem that I'm just not seeing.

Perhaps something from the AOP or DI domain could help.

evincarofautumn

You could have clients “subscribe” to different question types, registering their ability to answer them.

So you have one callback per question type, not one callback for all question types.

And if it’s ambiguous or unspecified how to answer a question, you have an error.

strager

But I don't want the application developer to have to register all of the mundane singleton answerers (case 1 above).

evincarofautumn

Would it not work to have a default set of those?

strager

Right now the application developer already has to specify the list of classes (Question/Answer types) they want to use. Otherwise, the Database has no idea how to deserialize data.

So perhaps registration of answerers could be coupled to that.

So I have thought about doing registration for another thing.

In a large project, it's pretty dumb to have a centralized place where you do "if libfoo.so, do this, else if libbar.so, do that"

Instead you would do "everything in the build/foo/ directory goes into this build system module; everything in the build/bar/ directory goes into that build system module"

Or (depending on how you write your application) "everything .dll goes into the 'linker' module, and everything .obj goes into the 'compiler' module"

Okay, how about this.

There is a mapping from Question type to some opaque type.

std::map<QuestionVTable , void >

Initially, it is empty.

You can add a QuestionVTable to the map.

For case 1 (singleton), the value in the map is nullptr.

For case 2 (dynamic), the value in the map is also nullptr.

For case 3 (application-overwritten), the value is some incrementally-built data structure.

For example, if I want to add a rule which matches a file path pattern, I could add that path pattern to the void*. (FileQuestion would need to provide a method for this.)

In all cases, the QuestionVTable's answer method is called with that void*.

The answer method will either use patterns specified by the application or use its fallback implementation.

This is kind of like how Make works (externally). You add rules to some global state.

libfoo.so: a.o b.o c.o

file_question_vtable->add(mymap, "libfoo.so", []() { /* build from {a,b,c}.o */ });

The problem with this approach is nesting. In order to be efficient, you don't want to add all rules up-front. That makes the build system O(n) (n is number of rules). I don't want that.

Instead, I want something like "look at these pattern->function mappings if this pattern mathes"

matches*

So let's call this std::map<QuestionVTable , void > a RuleSet.

Actually, ugh...

RuleSet doesn't make too much sense if the void* is just a function closure.

If it's just a function closure, you may as well subclass QuestionVTable.

But maybe the design makes more sense with function closures.

I dunno.

You probably want caching and some other shit for file path matching, to make things O(ln n).

Programming is hard. =[

Doing things The Easy Way is so easy but sooo wrong.

strager commented 9 years ago

After some thinking, I think I have a (non?-)solution.

In sort, KISS, don't overcomplicate things. We can rewrite later; let's make progress now.

strager commented 9 years ago

Ah! I cannot subclass! This will not work.

You can't subclass a B_QuestionVTable instance because callers of b_answer_context_need will not be able to get a handle to that B_QuestionVTable instance subclass.

strager commented 9 years ago

KISS. Things built on top of b which show actual pain points.