@@ -70,9 +70,8 @@ JUCE_BEGIN_NO_SANITIZE ("vptr") | |||||
#endif | #endif | ||||
#if JUCE_LINUX || JUCE_BSD | #if JUCE_LINUX || JUCE_BSD | ||||
#include "juce_events/native/juce_linux_EventLoopInternal.h" | |||||
#include <unordered_map> | #include <unordered_map> | ||||
std::vector<std::pair<int, std::function<void (int)>>> getFdReadCallbacks(); | |||||
#endif | #endif | ||||
#if JUCE_MAC | #if JUCE_MAC | ||||
@@ -103,14 +102,20 @@ using namespace Steinberg; | |||||
//============================================================================== | //============================================================================== | ||||
#if JUCE_LINUX || JUCE_BSD | #if JUCE_LINUX || JUCE_BSD | ||||
class EventHandler final : public Steinberg::Linux::IEventHandler | |||||
class EventHandler final : public Steinberg::Linux::IEventHandler, | |||||
private LinuxEventLoopInternal::Listener | |||||
{ | { | ||||
public: | public: | ||||
EventHandler() = default; | |||||
EventHandler() | |||||
{ | |||||
LinuxEventLoopInternal::registerLinuxEventLoopListener (*this); | |||||
} | |||||
~EventHandler() | |||||
~EventHandler() override | |||||
{ | { | ||||
jassert (hostRunLoops.size() == 0); | |||||
jassert (hostRunLoops.empty()); | |||||
LinuxEventLoopInternal::deregisterLinuxEventLoopListener (*this); | |||||
if (! messageThread->isRunning()) | if (! messageThread->isRunning()) | ||||
messageThread->start(); | messageThread->start(); | ||||
@@ -126,11 +131,7 @@ public: | |||||
void PLUGIN_API onFDIsSet (Steinberg::Linux::FileDescriptor fd) override | void PLUGIN_API onFDIsSet (Steinberg::Linux::FileDescriptor fd) override | ||||
{ | { | ||||
updateCurrentMessageThread(); | updateCurrentMessageThread(); | ||||
auto it = fdCallbackMap.find (fd); | |||||
if (it != fdCallbackMap.end()) | |||||
it->second (fd); | |||||
LinuxEventLoopInternal::invokeEventLoopCallbackForFd (fd); | |||||
} | } | ||||
//============================================================================== | //============================================================================== | ||||
@@ -138,19 +139,7 @@ public: | |||||
{ | { | ||||
if (auto* runLoop = getRunLoopFromFrame (plugFrame)) | if (auto* runLoop = getRunLoopFromFrame (plugFrame)) | ||||
{ | { | ||||
if (hostRunLoops.contains (runLoop)) | |||||
runLoop->unregisterEventHandler (this); | |||||
hostRunLoops.add (runLoop); | |||||
fdCallbackMap.clear(); | |||||
for (auto& cb : getFdReadCallbacks()) | |||||
{ | |||||
fdCallbackMap[cb.first] = cb.second; | |||||
runLoop->registerEventHandler (this, cb.first); | |||||
} | |||||
refreshAttachedEventLoop ([this, runLoop] { hostRunLoops.insert (runLoop); }); | |||||
updateCurrentMessageThread(); | updateCurrentMessageThread(); | ||||
} | } | ||||
} | } | ||||
@@ -158,67 +147,58 @@ public: | |||||
void unregisterHandlerForFrame (IPlugFrame* plugFrame) | void unregisterHandlerForFrame (IPlugFrame* plugFrame) | ||||
{ | { | ||||
if (auto* runLoop = getRunLoopFromFrame (plugFrame)) | if (auto* runLoop = getRunLoopFromFrame (plugFrame)) | ||||
{ | |||||
hostRunLoops.remove (runLoop); | |||||
if (! hostRunLoops.contains (runLoop)) | |||||
runLoop->unregisterEventHandler (this); | |||||
} | |||||
refreshAttachedEventLoop ([this, runLoop] { hostRunLoops.erase (runLoop); }); | |||||
} | } | ||||
private: | private: | ||||
//============================================================================= | |||||
class HostRunLoopInterfaces | |||||
//============================================================================== | |||||
/* Connects all known FDs to a single host event loop instance. */ | |||||
class AttachedEventLoop | |||||
{ | { | ||||
public: | public: | ||||
HostRunLoopInterfaces() = default; | |||||
AttachedEventLoop() = default; | |||||
void add (Steinberg::Linux::IRunLoop* runLoop) | |||||
AttachedEventLoop (Steinberg::Linux::IRunLoop* loopIn, Steinberg::Linux::IEventHandler* handlerIn) | |||||
: loop (loopIn), handler (handlerIn) | |||||
{ | { | ||||
if (auto* refCountedRunLoop = find (runLoop)) | |||||
{ | |||||
++(refCountedRunLoop->refCount); | |||||
return; | |||||
} | |||||
runLoops.push_back ({ runLoop, 1 }); | |||||
for (auto& fd : LinuxEventLoopInternal::getRegisteredFds()) | |||||
loop->registerEventHandler (handler, fd); | |||||
} | } | ||||
void remove (Steinberg::Linux::IRunLoop* runLoop) | |||||
AttachedEventLoop (AttachedEventLoop&& other) noexcept | |||||
{ | { | ||||
if (auto* refCountedRunLoop = find (runLoop)) | |||||
if (--(refCountedRunLoop->refCount) == 0) | |||||
runLoops.erase (std::find (runLoops.begin(), runLoops.end(), runLoop)); | |||||
swap (other); | |||||
} | } | ||||
size_t size() const noexcept { return runLoops.size(); } | |||||
bool contains (Steinberg::Linux::IRunLoop* runLoop) { return find (runLoop) != nullptr; } | |||||
private: | |||||
struct RefCountedRunLoop | |||||
AttachedEventLoop& operator= (AttachedEventLoop&& other) noexcept | |||||
{ | { | ||||
Steinberg::Linux::IRunLoop* runLoop = nullptr; | |||||
int refCount = 0; | |||||
swap (other); | |||||
return *this; | |||||
} | |||||
bool operator== (const Steinberg::Linux::IRunLoop* other) const noexcept { return runLoop == other; } | |||||
}; | |||||
AttachedEventLoop (const AttachedEventLoop&) = delete; | |||||
AttachedEventLoop& operator= (const AttachedEventLoop&) = delete; | |||||
RefCountedRunLoop* find (const Steinberg::Linux::IRunLoop* runLoop) | |||||
~AttachedEventLoop() | |||||
{ | { | ||||
auto iter = std::find (runLoops.begin(), runLoops.end(), runLoop); | |||||
if (iter != runLoops.end()) | |||||
return &(*iter); | |||||
if (loop == nullptr) | |||||
return; | |||||
return nullptr; | |||||
loop->unregisterEventHandler (handler); | |||||
} | } | ||||
std::vector<RefCountedRunLoop> runLoops; | |||||
private: | |||||
void swap (AttachedEventLoop& other) | |||||
{ | |||||
std::swap (other.loop, loop); | |||||
std::swap (other.handler, handler); | |||||
} | |||||
JUCE_DECLARE_NON_MOVEABLE (HostRunLoopInterfaces) | |||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HostRunLoopInterfaces) | |||||
Steinberg::Linux::IRunLoop* loop = nullptr; | |||||
Steinberg::Linux::IEventHandler* handler = nullptr; | |||||
}; | }; | ||||
//============================================================================== | |||||
static Steinberg::Linux::IRunLoop* getRunLoopFromFrame (IPlugFrame* plugFrame) | static Steinberg::Linux::IRunLoop* getRunLoopFromFrame (IPlugFrame* plugFrame) | ||||
{ | { | ||||
Steinberg::Linux::IRunLoop* runLoop = nullptr; | Steinberg::Linux::IRunLoop* runLoop = nullptr; | ||||
@@ -241,12 +221,44 @@ private: | |||||
} | } | ||||
} | } | ||||
void fdCallbacksChanged() override | |||||
{ | |||||
// The set of active FDs has changed, so deregister from the current event loop and then | |||||
// re-register the current set of FDs. | |||||
refreshAttachedEventLoop ([]{}); | |||||
} | |||||
/* Deregisters from any attached event loop, updates the set of known event loops, and then | |||||
attaches all FDs to the first known event loop. | |||||
The same event loop instance is shared between all plugin instances. Every time an event | |||||
loop is added or removed, this function should be called to register all FDs with a | |||||
suitable event loop. | |||||
Note that there's no API to deregister a single FD for a given event loop. Instead, we must | |||||
deregister all FDs, and then register all known FDs again. | |||||
*/ | |||||
template <typename Callback> | |||||
void refreshAttachedEventLoop (Callback&& modifyKnownRunLoops) | |||||
{ | |||||
// Deregister the old event loop. | |||||
// It's important to call the destructor from the old attached loop before calling the | |||||
// constructor of the new attached loop. | |||||
attachedEventLoop = AttachedEventLoop(); | |||||
modifyKnownRunLoops(); | |||||
// If we still know about an extant event loop, attach to it. | |||||
if (hostRunLoops.begin() != hostRunLoops.end()) | |||||
attachedEventLoop = AttachedEventLoop (*hostRunLoops.begin(), this); | |||||
} | |||||
SharedResourcePointer<MessageThread> messageThread; | SharedResourcePointer<MessageThread> messageThread; | ||||
std::atomic<int> refCount { 1 }; | std::atomic<int> refCount { 1 }; | ||||
HostRunLoopInterfaces hostRunLoops; | |||||
std::unordered_map<int, std::function<void (int)>> fdCallbackMap; | |||||
std::multiset<Steinberg::Linux::IRunLoop*> hostRunLoops; | |||||
AttachedEventLoop attachedEventLoop; | |||||
//============================================================================== | //============================================================================== | ||||
JUCE_DECLARE_NON_MOVEABLE (EventHandler) | JUCE_DECLARE_NON_MOVEABLE (EventHandler) | ||||
@@ -87,6 +87,7 @@ | |||||
#endif | #endif | ||||
#elif JUCE_LINUX || JUCE_BSD | #elif JUCE_LINUX || JUCE_BSD | ||||
#include "native/juce_linux_EventLoopInternal.h" | |||||
#include "native/juce_linux_Messaging.cpp" | #include "native/juce_linux_Messaging.cpp" | ||||
#elif JUCE_ANDROID | #elif JUCE_ANDROID | ||||
@@ -0,0 +1,55 @@ | |||||
/* | |||||
============================================================================== | |||||
This file is part of the JUCE library. | |||||
Copyright (c) 2022 - Raw Material Software Limited | |||||
JUCE is an open source library subject to commercial or open-source | |||||
licensing. | |||||
The code included in this file is provided under the terms of the ISC license | |||||
http://www.isc.org/downloads/software-support-policy/isc-license. Permission | |||||
To use, copy, modify, and/or distribute this software for any purpose with or | |||||
without fee is hereby granted provided that the above copyright notice and | |||||
this permission notice appear in all copies. | |||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER | |||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE | |||||
DISCLAIMED. | |||||
============================================================================== | |||||
*/ | |||||
namespace juce | |||||
{ | |||||
struct LinuxEventLoopInternal | |||||
{ | |||||
/** | |||||
@internal | |||||
Receives notifications when a fd callback is added or removed. | |||||
This is useful for VST3 plugins that host other VST3 plugins. In this scenario, the 'inner' | |||||
plugin may want to register additional file descriptors on top of those registered by the | |||||
'outer' plugin. In order for this to work, the outer plugin must in turn pass this request | |||||
on to the host, to indicate that the outer plugin wants to receive FD callbacks for both the | |||||
inner and outer plugin. | |||||
*/ | |||||
struct Listener | |||||
{ | |||||
virtual ~Listener() = default; | |||||
virtual void fdCallbacksChanged() = 0; | |||||
}; | |||||
/** @internal */ | |||||
static void registerLinuxEventLoopListener (Listener&); | |||||
/** @internal */ | |||||
static void deregisterLinuxEventLoopListener (Listener&); | |||||
/** @internal */ | |||||
static void invokeEventLoopCallbackForFd (int); | |||||
/** @internal */ | |||||
static std::vector<int> getRegisteredFds(); | |||||
}; | |||||
} // namespace juce |
@@ -108,125 +108,168 @@ private: | |||||
JUCE_IMPLEMENT_SINGLETON (InternalMessageQueue) | JUCE_IMPLEMENT_SINGLETON (InternalMessageQueue) | ||||
//============================================================================== | //============================================================================== | ||||
/* | |||||
Stores callbacks associated with file descriptors (FD). | |||||
The callback for a particular FD should be called whenever that file has data to read. | |||||
For standalone apps, the main thread will call poll to wait for new data on any FD, and then | |||||
call the associated callbacks for any FDs that changed. | |||||
For plugins, the host (generally) provides some kind of run loop mechanism instead. | |||||
- In VST2 plugins, the host should call effEditIdle at regular intervals, and plugins can | |||||
dispatch all pending events inside this callback. The host doesn't know about any of the | |||||
plugin's FDs, so it's possible there will be a bit of latency between an FD becoming ready, | |||||
and its associated callback being called. | |||||
- In VST3 plugins, it's possible to register each FD individually with the host. In this case, | |||||
the facilities in LinuxEventLoopInternal can be used to observe added/removed FD callbacks, | |||||
and the host can be notified whenever the set of FDs changes. The host will call onFDIsSet | |||||
whenever a particular FD has data ready. This call should be forwarded through to | |||||
InternalRunLoop::dispatchEvent. | |||||
*/ | |||||
struct InternalRunLoop | struct InternalRunLoop | ||||
{ | { | ||||
public: | public: | ||||
InternalRunLoop() | |||||
{ | |||||
fdReadCallbacks.reserve (16); | |||||
} | |||||
InternalRunLoop() = default; | |||||
void registerFdCallback (int fd, std::function<void (int)>&& cb, short eventMask) | |||||
void registerFdCallback (int fd, std::function<void()>&& cb, short eventMask) | |||||
{ | { | ||||
const ScopedLock sl (lock); | |||||
if (shouldDeferModifyingReadCallbacks) | |||||
{ | { | ||||
deferredReadCallbackModifications.emplace_back ([this, fd, cb, eventMask]() mutable | |||||
{ | |||||
registerFdCallback (fd, std::move (cb), eventMask); | |||||
}); | |||||
return; | |||||
const ScopedLock sl (lock); | |||||
callbacks.emplace (fd, std::make_shared<std::function<void()>> (std::move (cb))); | |||||
const auto iter = getPollfd (fd); | |||||
if (iter == pfds.end() || iter->fd != fd) | |||||
pfds.insert (iter, { fd, eventMask, 0 }); | |||||
else | |||||
jassertfalse; | |||||
jassert (pfdsAreSorted()); | |||||
} | } | ||||
fdReadCallbacks.push_back ({ fd, std::move (cb) }); | |||||
pfds.push_back ({ fd, eventMask, 0 }); | |||||
listeners.call ([] (auto& l) { l.fdCallbacksChanged(); }); | |||||
} | } | ||||
void unregisterFdCallback (int fd) | void unregisterFdCallback (int fd) | ||||
{ | { | ||||
const ScopedLock sl (lock); | |||||
if (shouldDeferModifyingReadCallbacks) | |||||
{ | { | ||||
deferredReadCallbackModifications.emplace_back ([this, fd] { unregisterFdCallback (fd); }); | |||||
return; | |||||
} | |||||
const ScopedLock sl (lock); | |||||
{ | |||||
auto removePredicate = [=] (const std::pair<int, std::function<void (int)>>& cb) { return cb.first == fd; }; | |||||
callbacks.erase (fd); | |||||
fdReadCallbacks.erase (std::remove_if (std::begin (fdReadCallbacks), std::end (fdReadCallbacks), removePredicate), | |||||
std::end (fdReadCallbacks)); | |||||
} | |||||
const auto iter = getPollfd (fd); | |||||
{ | |||||
auto removePredicate = [=] (const pollfd& pfd) { return pfd.fd == fd; }; | |||||
if (iter != pfds.end() && iter->fd == fd) | |||||
pfds.erase (iter); | |||||
else | |||||
jassertfalse; | |||||
pfds.erase (std::remove_if (std::begin (pfds), std::end (pfds), removePredicate), | |||||
std::end (pfds)); | |||||
jassert (pfdsAreSorted()); | |||||
} | } | ||||
listeners.call ([] (auto& l) { l.fdCallbacksChanged(); }); | |||||
} | } | ||||
bool dispatchPendingEvents() | bool dispatchPendingEvents() | ||||
{ | { | ||||
const ScopedLock sl (lock); | |||||
callbackStorage.clear(); | |||||
getFunctionsToCallThisTime (callbackStorage); | |||||
if (poll (&pfds.front(), static_cast<nfds_t> (pfds.size()), 0) == 0) | |||||
return false; | |||||
// CriticalSection should be available during the callback | |||||
for (auto& fn : callbackStorage) | |||||
(*fn)(); | |||||
bool eventWasSent = false; | |||||
return ! callbackStorage.empty(); | |||||
} | |||||
for (auto& pfd : pfds) | |||||
void dispatchEvent (int fd) const | |||||
{ | |||||
const auto fn = [&] | |||||
{ | { | ||||
if (pfd.revents == 0) | |||||
continue; | |||||
const ScopedLock sl (lock); | |||||
const auto iter = callbacks.find (fd); | |||||
return iter != callbacks.end() ? iter->second : nullptr; | |||||
}(); | |||||
// CriticalSection should be available during the callback | |||||
if (auto* callback = fn.get()) | |||||
(*callback)(); | |||||
} | |||||
bool sleepUntilNextEvent (int timeoutMs) | |||||
{ | |||||
const ScopedLock sl (lock); | |||||
return poll (pfds.data(), static_cast<nfds_t> (pfds.size()), timeoutMs) != 0; | |||||
} | |||||
std::vector<int> getRegisteredFds() | |||||
{ | |||||
const ScopedLock sl (lock); | |||||
std::vector<int> result; | |||||
result.reserve (callbacks.size()); | |||||
std::transform (callbacks.begin(), | |||||
callbacks.end(), | |||||
std::back_inserter (result), | |||||
[] (const auto& pair) { return pair.first; }); | |||||
return result; | |||||
} | |||||
void addListener (LinuxEventLoopInternal::Listener& listener) { listeners.add (&listener); } | |||||
void removeListener (LinuxEventLoopInternal::Listener& listener) { listeners.remove (&listener); } | |||||
pfd.revents = 0; | |||||
//============================================================================== | |||||
JUCE_DECLARE_SINGLETON (InternalRunLoop, false) | |||||
private: | |||||
using SharedCallback = std::shared_ptr<std::function<void()>>; | |||||
/* Appends any functions that need to be called to the passed-in vector. | |||||
auto fd = pfd.fd; | |||||
We take a copy of each shared function so that the functions can be called without | |||||
locking or racing in the event that the function attempts to register/deregister a | |||||
new FD callback. | |||||
*/ | |||||
void getFunctionsToCallThisTime (std::vector<SharedCallback>& functions) | |||||
{ | |||||
const ScopedLock sl (lock); | |||||
if (! sleepUntilNextEvent (0)) | |||||
return; | |||||
for (auto& fdAndCallback : fdReadCallbacks) | |||||
for (auto& pfd : pfds) | |||||
{ | |||||
if (std::exchange (pfd.revents, 0) != 0) | |||||
{ | { | ||||
if (fdAndCallback.first == fd) | |||||
{ | |||||
{ | |||||
ScopedValueSetter<bool> insideFdReadCallback (shouldDeferModifyingReadCallbacks, true); | |||||
fdAndCallback.second (fd); | |||||
} | |||||
if (! deferredReadCallbackModifications.empty()) | |||||
{ | |||||
for (auto& deferredRegisterEvent : deferredReadCallbackModifications) | |||||
deferredRegisterEvent(); | |||||
deferredReadCallbackModifications.clear(); | |||||
// elements may have been removed from the fdReadCallbacks/pfds array so we really need | |||||
// to call poll again | |||||
return true; | |||||
} | |||||
eventWasSent = true; | |||||
} | |||||
const auto iter = callbacks.find (pfd.fd); | |||||
if (iter != callbacks.end()) | |||||
functions.emplace_back (iter->second); | |||||
} | } | ||||
} | } | ||||
return eventWasSent; | |||||
} | } | ||||
void sleepUntilNextEvent (int timeoutMs) | |||||
std::vector<pollfd>::iterator getPollfd (int fd) | |||||
{ | { | ||||
poll (&pfds.front(), static_cast<nfds_t> (pfds.size()), timeoutMs); | |||||
return std::lower_bound (pfds.begin(), pfds.end(), fd, [] (auto descriptor, auto toFind) | |||||
{ | |||||
return descriptor.fd < toFind; | |||||
}); | |||||
} | } | ||||
std::vector<std::pair<int, std::function<void (int)>>> getFdReadCallbacks() | |||||
bool pfdsAreSorted() const | |||||
{ | { | ||||
const ScopedLock sl (lock); | |||||
return fdReadCallbacks; | |||||
return std::is_sorted (pfds.begin(), pfds.end(), [] (auto a, auto b) { return a.fd < b.fd; }); | |||||
} | } | ||||
//============================================================================== | |||||
JUCE_DECLARE_SINGLETON (InternalRunLoop, false) | |||||
private: | |||||
CriticalSection lock; | CriticalSection lock; | ||||
std::vector<std::pair<int, std::function<void (int)>>> fdReadCallbacks; | |||||
std::map<int, SharedCallback> callbacks; | |||||
std::vector<SharedCallback> callbackStorage; | |||||
std::vector<pollfd> pfds; | std::vector<pollfd> pfds; | ||||
bool shouldDeferModifyingReadCallbacks = false; | |||||
std::vector<std::function<void()>> deferredReadCallbackModifications; | |||||
ListenerList<LinuxEventLoopInternal::Listener> listeners; | |||||
}; | }; | ||||
JUCE_IMPLEMENT_SINGLETON (InternalRunLoop) | JUCE_IMPLEMENT_SINGLETON (InternalRunLoop) | ||||
@@ -313,7 +356,7 @@ bool dispatchNextMessageOnSystemQueue (bool returnIfNoPendingMessages) | |||||
void LinuxEventLoop::registerFdCallback (int fd, std::function<void (int)> readCallback, short eventMask) | void LinuxEventLoop::registerFdCallback (int fd, std::function<void (int)> readCallback, short eventMask) | ||||
{ | { | ||||
if (auto* runLoop = InternalRunLoop::getInstanceWithoutCreating()) | if (auto* runLoop = InternalRunLoop::getInstanceWithoutCreating()) | ||||
runLoop->registerFdCallback (fd, std::move (readCallback), eventMask); | |||||
runLoop->registerFdCallback (fd, [cb = std::move (readCallback), fd] { cb (fd); }, eventMask); | |||||
} | } | ||||
void LinuxEventLoop::unregisterFdCallback (int fd) | void LinuxEventLoop::unregisterFdCallback (int fd) | ||||
@@ -322,16 +365,31 @@ void LinuxEventLoop::unregisterFdCallback (int fd) | |||||
runLoop->unregisterFdCallback (fd); | runLoop->unregisterFdCallback (fd); | ||||
} | } | ||||
} // namespace juce | |||||
//============================================================================== | |||||
void LinuxEventLoopInternal::registerLinuxEventLoopListener (LinuxEventLoopInternal::Listener& listener) | |||||
{ | |||||
if (auto* runLoop = InternalRunLoop::getInstanceWithoutCreating()) | |||||
runLoop->addListener (listener); | |||||
} | |||||
void LinuxEventLoopInternal::deregisterLinuxEventLoopListener (LinuxEventLoopInternal::Listener& listener) | |||||
{ | |||||
if (auto* runLoop = InternalRunLoop::getInstanceWithoutCreating()) | |||||
runLoop->removeListener (listener); | |||||
} | |||||
JUCE_API std::vector<std::pair<int, std::function<void (int)>>> getFdReadCallbacks(); | |||||
JUCE_API std::vector<std::pair<int, std::function<void (int)>>> getFdReadCallbacks() | |||||
void LinuxEventLoopInternal::invokeEventLoopCallbackForFd (int fd) | |||||
{ | { | ||||
using namespace juce; | |||||
if (auto* runLoop = InternalRunLoop::getInstanceWithoutCreating()) | |||||
runLoop->dispatchEvent (fd); | |||||
} | |||||
std::vector<int> LinuxEventLoopInternal::getRegisteredFds() | |||||
{ | |||||
if (auto* runLoop = InternalRunLoop::getInstanceWithoutCreating()) | if (auto* runLoop = InternalRunLoop::getInstanceWithoutCreating()) | ||||
return runLoop->getFdReadCallbacks(); | |||||
return runLoop->getRegisteredFds(); | |||||
jassertfalse; | |||||
return {}; | return {}; | ||||
} | } | ||||
} // namespace juce |