Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Unlimited" buses #4529

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft

"Unlimited" buses #4529

wants to merge 6 commits into from

Conversation

blazoncek
Copy link
Collaborator

Add ability to have up to 36 (theoretical limit) buses, physical or virtual.

Stuff also done:

  • converted BusManager from class to namespace
  • modified BusConfig to use vector
  • modified ColorOrderMap::getPixelColorOrder() to use vector (and removed ColorOrderMap from BusDigital)
  • updated loading config into UI (LED settings, buttons, etc.)

WARNING web server may not support that many variables when creating buses so actual limit may be less.

Many thanks to @willmmiles @TripleWhy and @PaoloTK for helpful comments and suggestions.

- use unique_ptr/make_unique for busses
- added config upload options
- number of buses it limited to 36 (0-9+A-Z identifiers)
- WRNING web server may not support that many variables
@blazoncek
Copy link
Collaborator Author

Apparently will require #4484 to be merged 1st.

@blazoncek blazoncek marked this pull request as draft January 30, 2025 19:40
wled00/bus_manager.h Outdated Show resolved Hide resolved
// Really simple C++11 shim for non-array case; implementation from cppreference.com
template<class T, class... Args>
std::unique_ptr<T>
make_unique(Args&&... args)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be moved to a different header file

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but I want to limit number of modified files for transparency sake.
Once toolchain is updated we may not need this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for transparency sake

The git diff won't look much different

Once toolchain is updated

Will that actually happen? ^^
Anyway it should be easier and cleaner to delete a polyfill.h than to delete a function here and check if other files included bus_manager.h just for the sake of make_unique

@@ -78,7 +93,7 @@ class Bus {
_autoWhiteMode = Bus::hasWhite(type) ? aw : RGBW_MODE_MANUAL_ONLY;
};

virtual ~Bus() {} //throw the bus under the bus
virtual ~Bus() {} //throw the bus under the bus (derived class needs to freeData())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One way to make this safer would be to call freeData() here, and then override freeData() in the busses that don't need to do anything with an empty function

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate a bit more?
I was thinking if Bus does not allocate any data for itself (but provides non-virtual methods) a derived class should take care of deallocation if it allocates anything.
My first thoughts were to implement automatic deallocation upon destruction, but some derived classes assign _data pointer to class member variables to reduce heap fragmentation. In such case no deallocation should happen.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about something like this:

//Bus
virtual Bus::~Bus() { freeData(); }
virtual void Bus::freeData() { if (_data != nullptr) free(_data); _data = nullptr; }

//Bus1 uses _data and doesn't override freeData()

//Bus2 doesn't use _data
virtual void Bus::freeData() {}

However:
freeData checks if _data is nullptr, so really I don't think you'd have to do anything like that:

//Bus
virtual Bus::~Bus() { freeData(); }
void Bus::freeData() { if (_data != nullptr) free(_data); _data = nullptr; }

//Bus1 uses _data and doesn't override freeData()
//Bus2 doesn't use _data and doesn't override freeData()

Should work just fine, shouldn't it?

Another option would be to move _data into the sub classes, so each class would be responsible for their own memory.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option would be to move _data into the sub classes, so each class would be responsible for their own memory.

This would be my preferred option. It also lets each class use type-safe storage and descriptive names for their state data as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find that counterintuitive.

All derived classes may find use of _data pointer but some may allocate space on heap, some on stack (or on heap within its instance). For those that use heap, allocateData() and freeData() are provided.
If I understand virtual methods correctly if you override virtual base method in a derived class then you have to call base method explicitly if you want to use its implementation. I.e.

void Derived::freeData() {
  ... some stuff ...
  Base::freeData();
  ... more stuff ...
}

or implement functionality of Base::freeData() in derived (a nonsense). Having virtual Base::freeData() that does nothing (and is not pure virtual) and having pure virtual (that needs to be implemented in derived class) seems counterproductive in this particular instance as allocateData() does something.

As allocateData() and freeData() are not virtual, two less pointers need to be allocated for derived class' vtables (which is good in our case). Of course that means that when derived class uses heap allocated data, it needs to call allocateData() at some point and then (at least in destructor) call freeData(). All is well and also well defined.
One could add heap/stack detection into virtual Base::freeData() (if that approach is taken) so that correct de-allocation may happen, but I think that is a hack best avoided as it is architecture dependant.

BTW: All of the derived classes use _data pointer in one way or another, but only BusDigital and BusNetwork use heap allocated buffers. When I first implemented buffers, _data was defined in those two derived classes (as suggested), but that meant to also implement 2 allocateData() and 2 freeData() methods. I found that redundant and prone to error more than forgetting to call base class freeData().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need allocateData or freeData?

allocateData is called exactly once, during construction (in 2 classes). Which means there is no need for gating or freeing _data before allocation. The zero length check is probably also unnecessary, leaving only one effective call: calloc. Does it make sense to abstract that? Not really. Does it improve anything to have allocateData that can be used in two classes instead of calloc (which can also be used in both classes)? Well, no.

What about freeData? Once again, it is called exactly once, during destruction (in 2 classes). Oh but it performs a nullptr check, that has to be useful, right? well, no; free does nothing when called with nullptr. Ok but it also sets _data to nullptr when it's done. Which sounds like a good safety move, but remember, this happens during destruction, so even reading the value _data would be unsafe. So really this function can be replaced with a single free call as well.

About the error-proneness of this all: using calloc and free is error prone. If you are worried about that, consider unique_ptr or other type safe per-class solutions instead :)

Copy link
Collaborator Author

@blazoncek blazoncek Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about Bus::_data then? It should also be eliminated and moved into those 2 classes that use it. Right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, in an ideal world, yes. I'm not sure how other functions that currently use _data would work then though.

wled00/bus_manager.h Outdated Show resolved Hide resolved
extern std::vector<std::unique_ptr<Bus>> busses;
//extern std::vector<Bus*> busses;
extern uint16_t _gMilliAmpsUsed;
extern uint16_t _gMilliAmpsMax;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think adding public and extern gloabals is a really bad idea

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if enclosed in namespace?
I could do without but then functions could no longer be inline (not that this is good or bad).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • They are public now. Anyone can access and modify them. Functional safety isn't about not doing the wrong thing because you know what you are doing, its about preventing the wrong thing even if not everyone knows what they are doing.
  • You now have to separate definition and initialization.
  • extern can prevent optimizations

Avoiding this would work by keeping the class or moving the variables to the cpp. I don't know how the latter affects embedded code though.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoiding this would work by keeping the class or moving the variables to the cpp. I don't know how the latter affects embedded code though.

Moving the declarations in to the cpp would give up any inlined access -- everything would have to be a function call. We don't have link-time optimization to help us here. :(

The previous code (a non-instantiated class, where everything is static) is the only way I know of to get both access control and still support inlining.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WLED is full of globals, accessible/accessed throughout the code.
If you'll look many may be shadowed in certain functions which is prone to error (forget or accidentally remove definition and you are using global).
Using namespaced variable reduces the possibility to inadvertently use such global. But yes, I agree that having globals is poor programming practice.

As we discuss these issues it may indeed be better to forego namespace and keep using instanceless class.

- as suggested by @TripleWhy
- minimum length guard

Conflicts:
	wled00/bus_manager.cpp
	wled00/bus_manager.h
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants