-
-
Notifications
You must be signed in to change notification settings - Fork 120
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
Refactor code to use abstract classes and PIMPL #370
base: main
Are you sure you want to change the base?
Conversation
This paves the way towards having multiple implementations at runtime. It also centralized the defintions of the "base" or "implementation" classes (such as AdapterBase, PeripheralBase) so that by explicitly deriving from these ABCs the interfaces of each implemetations are correct. The public interface for "frontend" classes (the ones used by the library user) are unchanged. The main differences are: - They now include an "internal_" shared pointer (the PIMPL) - Some logic is implemented in the frontend (i.e. checking that a peripheral is connected before invoking read_inner, write_inner, etc). - The logic to detect if the instance is initialized is centralized into a private "operator->()" - The "safe" classes no longer inherit from the non safe ones. They are castable, though, so most code should continue working unmodified. An additional concept, with the associated classes is introduced, the "Backend". For the time being this is not exposed in the public API. The idea is that all static methods in the Adapter will be moved to the Backend as non-static methods and the Backend frontend class will have a static get_backends(). Note that in many cases having a "BackendIMPL.h" header is unncecessary and thus ommited.
2e810c4
to
c4d1cde
Compare
The windows build fails on CI because it complains that a prototype declared "extern" is not called. Maybe the line that specifies WX should be removed:
Or I can add some MSVC-specifi pragmas, or I can figure out how to do the backend selection differently. |
static std::shared_ptr<BackendBase> _get_enabled_backend(){ | ||
SIMPLEBLE_ENABLE(BACKEND_LINUX) SIMPLEBLE_ENABLE(BACKEND_WINDOWS) SIMPLEBLE_ENABLE(BACKEND_ANDROID) | ||
SIMPLEBLE_ENABLE(BACKEND_MACOS) SIMPLEBLE_ENABLE(BACKEND_PLAIN)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think using plain IFDEFs is OK for this one, no need to make it overly complicated.
|
||
virtual std::vector<std::shared_ptr<AdapterBase>> get_adapters() = 0; | ||
virtual bool bluetooth_enabled() = 0; | ||
virtual std::string backend_name() const noexcept = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just call the method name
? Or does it conflict somewhere?
@@ -1,105 +1,99 @@ | |||
#include "fmt/format.h" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this header used here?
virtual ByteArray read_inner(BluetoothUUID const& service, BluetoothUUID const& characteristic) = 0; | ||
virtual void write_request_inner(BluetoothUUID const& service, BluetoothUUID const& characteristic, ByteArray const& data) = 0; | ||
virtual void write_command_inner(BluetoothUUID const& service, BluetoothUUID const& characteristic, ByteArray const& data) = 0; | ||
virtual void notify_inner(BluetoothUUID const& service, BluetoothUUID const& characteristic, std::function<void(ByteArray payload)> callback) = 0; | ||
virtual void indicate_inner(BluetoothUUID const& service, BluetoothUUID const& characteristic, std::function<void(ByteArray payload)> callback) = 0; | ||
virtual void unsubscribe_inner(BluetoothUUID const& service, BluetoothUUID const& characteristic) = 0; | ||
|
||
virtual ByteArray read_inner(BluetoothUUID const& service, BluetoothUUID const& characteristic, BluetoothUUID const& descriptor) = 0; | ||
virtual void write_inner(BluetoothUUID const& service, BluetoothUUID const& characteristic, BluetoothUUID const& descriptor, ByteArray const& data) = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we skip adding the _inner
suffix? It's sort of understandable if you're calling the internal
pointer that you're referring to an inner method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I put the _inner to indicate that it is not just that the frontend method calls the implementation method, but it also checks whether it is connected, so the _inner methods are only called on a connected peripheral. If that indication is not important, I can change the name back.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's important to have that distinction. I'd rather keep the same name.
/** | ||
* Cast to the underlying adapter object. | ||
*/ | ||
operator SimpleBLE::Adapter() const noexcept; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this implemented? I couldn't find it.
|
||
namespace SimpleBLE { | ||
template <typename T> | ||
using vec_of_shared = std::vector<std::shared_ptr<T>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use SharedPtrVector
instead?
|
||
void* AdapterBase::underlying() const { return adapter_.get(); } | ||
// TODO: why is this necessary? | ||
void* AdapterLinux::underlying() const { return adapter_.get(); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can add a developer note for this one. For certain compatibility with external libraries, we sometimes need to expose the actual OS handle to the user. This is particularly important for MacOS right now, but we might do something different in the future.
|
||
return enabled; | ||
} | ||
bool AdapterLinux::bluetooth_enabled() { return adapter_->powered(); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't match what the original implementation did. Unfortunately we need to check every individual adapter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, on a second pass, given that this functionality is currently provided by the backend, this function doesn't need to exist as part of the adapter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing with bluetooth_enabled is that there are different levels of granularity supported by different systems. Up to now, it was a static method, which means it reported the system-wide (and thus backend-wide, because only one backend could be enabled) setting.
In this PR I split the adapter-specific and backend-general enabled settings (which for some systems is the same).
On linux, it seems like one can query the powered status of individual adapters separately and the implementation was emulating a system-wide setting by returning true if at least one adapter was enabled. A problem with this is that the specific adapter corresponding to the Adapter object that the user has may not be the one that is enabled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we'll need to redesign this at some point, but I think for now your approach is fine, let's ship it.
|
||
// There is no need to make this class a singleton, as there is no state (no | ||
// member variables) | ||
class BackendMac : public BackendBase { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you rename this class and file to BackendCoreBluetooth
?
|
||
std::shared_ptr<BackendBase> BACKEND_WINDOWS() { return BackendWindows::get(); } | ||
|
||
BackendWindows::BackendWindows(buildToken) { initialize_winrt(); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we rename this backend to BackendWinRT
?
protected: | ||
AdapterBase* operator->(); | ||
const AdapterBase* operator->() const; | ||
|
||
std::shared_ptr<AdapterBase> internal_; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kdewald This repetition in each frontend class could be avoided by having a base PIMPL class that is (privately) inherited. I did not do it not to make the header look "complicated" but it would save on code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine having the duplication at this point, the most important thing is that the user-facing API is easy to read and understand right away. The extra lines of code are negligible.
This paves the way towards having multiple implementations at runtime. It also centralized the defintions of the "base" or "implementation" classes (such as AdapterBase, PeripheralBase) so that by explicitly deriving from these ABCs the interfaces of each implemetations are correct.
The public interface for "frontend" classes (the ones used by the library user) are unchanged. The main differences are:
An additional concept, with the associated classes is introduced, the "Backend". For the time being this is not exposed in the public API. The idea is that all static methods in the Adapter will be moved to the Backend as non-static methods and the Backend frontend class will have a static get_backends(). Note that in many cases having a "BackendIMPL.h" header is unncecessary and thus ommited.