summaryrefslogtreecommitdiff
path: root/libs/pbd
diff options
context:
space:
mode:
Diffstat (limited to 'libs/pbd')
-rw-r--r--libs/pbd/base_ui.cc85
-rw-r--r--libs/pbd/crossthread.cc98
-rw-r--r--libs/pbd/pbd/abstract_ui.cc129
-rw-r--r--libs/pbd/pbd/abstract_ui.h33
-rw-r--r--libs/pbd/pbd/base_ui.h33
-rw-r--r--libs/pbd/pbd/crossthread.h53
-rw-r--r--libs/pbd/pbd/pthread_utils.h9
-rw-r--r--libs/pbd/pthread_utils.cc45
-rw-r--r--libs/pbd/wscript1
9 files changed, 292 insertions, 194 deletions
diff --git a/libs/pbd/base_ui.cc b/libs/pbd/base_ui.cc
index 015951f118..259a51d954 100644
--- a/libs/pbd/base_ui.cc
+++ b/libs/pbd/base_ui.cc
@@ -33,36 +33,24 @@
using namespace std;
using namespace PBD;
+using namespace Glib;
-uint32_t BaseUI::rt_bit = 1;
+uint64_t BaseUI::rt_bit = 1;
BaseUI::RequestType BaseUI::CallSlot = BaseUI::new_request_type();
-BaseUI::BaseUI (string str, bool with_signal_pipe)
- : _name (str)
+BaseUI::BaseUI (const string& str)
+ : run_loop_thread (0)
+ , _name (str)
{
- /* odd pseudo-singleton semantics */
-
base_ui_instance = this;
- signal_pipe[0] = -1;
- signal_pipe[1] = -1;
+ request_channel.ios()->connect (sigc::mem_fun (*this, &BaseUI::request_handler));
- if (with_signal_pipe) {
- if (setup_signal_pipe ()) {
- throw failed_constructor ();
- }
- }
+ /* derived class must set _ok */
}
BaseUI::~BaseUI()
{
- if (signal_pipe[0] >= 0) {
- close (signal_pipe[0]);
- }
-
- if (signal_pipe[1] >= 0) {
- close (signal_pipe[1]);
- }
}
BaseUI::RequestType
@@ -78,32 +66,53 @@ BaseUI::new_request_type ()
return rt;
}
-int
-BaseUI::setup_signal_pipe ()
+void
+BaseUI::main_thread ()
+{
+ thread_init ();
+ _main_loop->run ();
+}
+
+void
+BaseUI::run ()
{
- /* setup the pipe that other threads send us notifications/requests
- through.
+ /* to be called by UI's that need/want their own distinct, self-created event loop thread.
+ Derived classes should have set up a handler for IO on request_channel.ios()
*/
- if (pipe (signal_pipe)) {
- error << string_compose (_("%1-UI: cannot create error signal pipe (%2)"), _name, ::strerror (errno))
- << endmsg;
+ _main_loop = MainLoop::create (MainContext::create());
+ request_channel.ios()->attach (_main_loop->get_context());
+ run_loop_thread = Thread::create (mem_fun (*this, &BaseUI::main_thread), true);
+}
- return -1;
- }
+void
+BaseUI::quit ()
+{
+ _main_loop->quit ();
+ run_loop_thread->join ();
+}
+
+bool
+BaseUI::request_handler (Glib::IOCondition ioc)
+{
+ /* check the transport request pipe */
- if (fcntl (signal_pipe[0], F_SETFL, O_NONBLOCK)) {
- error << string_compose (_("%1-UI: cannot set O_NONBLOCK on signal read pipe (%2)"), _name, ::strerror (errno))
- << endmsg;
- return -1;
+ if (ioc & ~IO_IN) {
+ _main_loop->quit ();
}
- if (fcntl (signal_pipe[1], F_SETFL, O_NONBLOCK)) {
- error << string_compose (_("%1-UI: cannot set O_NONBLOCK on signal write pipe (%2)"), _name, ::strerror (errno))
- << endmsg;
- return -1;
+ if (ioc & IO_IN) {
+ request_channel.drain ();
+
+ /* there may been an error. we'd rather handle requests first,
+ and then get IO_HUP or IO_ERR on the next loop.
+ */
+
+ /* handle requests */
+
+ handle_ui_requests ();
}
- return 0;
+ return true;
}
-
+
diff --git a/libs/pbd/crossthread.cc b/libs/pbd/crossthread.cc
new file mode 100644
index 0000000000..1465505b56
--- /dev/null
+++ b/libs/pbd/crossthread.cc
@@ -0,0 +1,98 @@
+/*
+ Copyright (C) 2009 Paul Davis
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <cstdlib>
+#include <cerrno>
+#include <cstring>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "pbd/error.h"
+#include "pbd/crossthread.h"
+
+using namespace std;
+using namespace PBD;
+using namespace Glib;
+
+CrossThreadChannel::CrossThreadChannel ()
+{
+ fds[0] = -1;
+ fds[1] = -1;
+
+ if (pipe (fds)) {
+ error << "cannot create x-thread pipe for read (%2)" << ::strerror (errno) << endmsg;
+ return;
+ }
+
+ if (fcntl (fds[0], F_SETFL, O_NONBLOCK)) {
+ error << "cannot set non-blocking mode for x-thread pipe (read) (" << ::strerror (errno) << ')' << endmsg;
+ return;
+ }
+
+ if (fcntl (fds[1], F_SETFL, O_NONBLOCK)) {
+ error << "cannot set non-blocking mode for x-thread pipe (write) (%2)" << ::strerror (errno) << ')' << endmsg;
+ return;
+ }
+
+}
+
+CrossThreadChannel::~CrossThreadChannel ()
+{
+ _ios->destroy ();
+
+ if (fds[0] >= 0) {
+ close (fds[0]);
+ fds[0] = -1;
+ }
+
+ if (fds[1] >= 0) {
+ close (fds[1]);
+ fds[1] = -1;
+ }
+}
+
+void
+CrossThreadChannel::wakeup ()
+{
+ char c = 0;
+ ::write (fds[1], &c, 1);
+}
+
+RefPtr<IOSource>
+CrossThreadChannel::ios ()
+{
+ if (!_ios) {
+ _ios = IOSource::create (fds[0], IOCondition(IO_IN|IO_PRI|IO_ERR|IO_HUP|IO_NVAL));
+ }
+ return _ios;
+}
+
+void
+CrossThreadChannel::drain ()
+{
+ drain (fds[0]);
+}
+
+void
+CrossThreadChannel::drain (int fd)
+{
+ /* drain selectable fd */
+ char buf[64];
+ while (::read (fd, buf, sizeof (buf)) > 0);
+}
diff --git a/libs/pbd/pbd/abstract_ui.cc b/libs/pbd/pbd/abstract_ui.cc
index 25c198774c..07f6d3a4b1 100644
--- a/libs/pbd/pbd/abstract_ui.cc
+++ b/libs/pbd/pbd/abstract_ui.cc
@@ -1,5 +1,6 @@
#include <unistd.h>
+#include "pbd/stacktrace.h"
#include "pbd/abstract_ui.h"
#include "pbd/pthread_utils.h"
#include "pbd/failed_constructor.h"
@@ -9,81 +10,65 @@
using namespace std;
template <typename RequestObject>
-AbstractUI<RequestObject>::AbstractUI (string name, bool with_signal_pipes)
- : BaseUI (name, with_signal_pipes)
+AbstractUI<RequestObject>::AbstractUI (const string& name)
+ : BaseUI (name)
{
- if (pthread_key_create (&thread_request_buffer_key, 0)) {
- cerr << _("cannot create thread request buffer key") << endl;
- throw failed_constructor();
- }
-
- PBD::ThreadCreatedWithRequestSize.connect (mem_fun (*this, &AbstractUI<RequestObject>::register_thread_with_request_count));
+ PBD::ThreadCreatedWithRequestSize.connect (mem_fun (*this, &AbstractUI<RequestObject>::register_thread));
}
template <typename RequestObject> void
-AbstractUI<RequestObject>::register_thread (pthread_t thread_id, string name)
+AbstractUI<RequestObject>::register_thread (string target_gui, pthread_t thread_id, string /*thread_name*/, uint32_t num_requests)
{
- register_thread_with_request_count (thread_id, name, 256);
-}
+ if (target_gui != name()) {
+ return;
+ }
-template <typename RequestObject> void
-AbstractUI<RequestObject>::register_thread_with_request_count (pthread_t thread_id, string /*thread_name*/, uint32_t num_requests)
-{
RequestBuffer* b = new RequestBuffer (num_requests);
{
- Glib::Mutex::Lock lm (request_buffer_map_lock);
+ Glib::Mutex::Lock lm (request_buffer_map_lock);
request_buffers[thread_id] = b;
}
- pthread_setspecific (thread_request_buffer_key, b);
+ per_thread_request_buffer.set (b);
}
template <typename RequestObject> RequestObject*
AbstractUI<RequestObject>::get_request (RequestType rt)
{
- RequestBuffer* rbuf = static_cast<RequestBuffer*>(pthread_getspecific (thread_request_buffer_key));
-
- if (rbuf == 0) {
- /* Cannot happen, but if it does we can't use the error reporting mechanism */
- cerr << _("programming error: ")
- << string_compose ("no %1-UI request buffer found for thread %2", name(), pthread_name())
- << endl;
- abort ();
- }
-
+ RequestBuffer* rbuf = per_thread_request_buffer.get ();
RequestBufferVector vec;
- vec.buf[0] = 0;
- vec.buf[1] = 0;
-
- rbuf->get_write_vector (&vec);
- if (vec.len[0] == 0) {
- if (vec.len[1] == 0) {
- cerr << string_compose ("no space in %1-UI request buffer for thread %2", name(), pthread_name())
- << endl;
+ if (rbuf != 0) {
+ /* we have a per-thread FIFO, use it */
+
+ rbuf->get_write_vector (&vec);
+
+ if (vec.len[0] == 0) {
return 0;
- } else {
- vec.buf[1]->type = rt;
- return vec.buf[1];
}
- } else {
+
vec.buf[0]->type = rt;
return vec.buf[0];
}
+
+ RequestObject* req = new RequestObject;
+ req->type = rt;
+ return req;
}
template <typename RequestObject> void
AbstractUI<RequestObject>::handle_ui_requests ()
{
RequestBufferMapIterator i;
+ RequestBufferVector vec;
+
+ /* per-thread buffers first */
request_buffer_map_lock.lock ();
for (i = request_buffers.begin(); i != request_buffers.end(); ++i) {
- RequestBufferVector vec;
-
while (true) {
/* we must process requests 1 by 1 because
@@ -110,6 +95,22 @@ AbstractUI<RequestObject>::handle_ui_requests ()
}
request_buffer_map_lock.unlock ();
+
+ /* and now, the generic request buffer. same rules as above apply */
+
+ Glib::Mutex::Lock lm (request_list_lock);
+
+ while (!request_list.empty()) {
+ RequestObject* req = request_list.front ();
+ request_list.pop_front ();
+ lm.release ();
+
+ do_request (req);
+
+ delete req;
+
+ lm.acquire();
+ }
}
template <typename RequestObject> void
@@ -118,31 +119,41 @@ AbstractUI<RequestObject>::send_request (RequestObject *req)
if (base_instance() == 0) {
return; /* XXX is this the right thing to do ? */
}
-
- if (caller_is_ui_thread()) {
- // cerr << "GUI thread sent request " << req << " type = " << req->type << endl;
+
+ if (caller_is_self ()) {
do_request (req);
} else {
- RequestBuffer* rbuf = static_cast<RequestBuffer*> (pthread_getspecific (thread_request_buffer_key));
+ RequestBuffer* rbuf = per_thread_request_buffer.get ();
- if (rbuf == 0) {
- /* can't use the error system to report this, because this
- thread isn't registered!
+ if (rbuf != 0) {
+ rbuf->increment_write_ptr (1);
+ } else {
+ /* no per-thread buffer, so just use a list with a lock so that it remains
+ single-reader/single-writer semantics
*/
- cerr << _("programming error: ")
- << string_compose ("AbstractUI::send_request() called from %1 (%2), but no request buffer exists for that thread", name(), pthread_name())
- << endl;
- abort ();
+ Glib::Mutex::Lock lm (request_list_lock);
+ request_list.push_back (req);
}
-
- // cerr << "thread " << pthread_self() << " sent request " << req << " type = " << req->type << endl;
- rbuf->increment_write_ptr (1);
-
- if (signal_pipe[1] >= 0) {
- const char c = 0;
- write (signal_pipe[1], &c, 1);
- }
+ request_channel.wakeup ();
}
}
+template<typename RequestObject> void
+AbstractUI<RequestObject>::call_slot (sigc::slot<void> elSlot)
+{
+ if (caller_is_self()) {
+ elSlot ();
+ return;
+ }
+
+ RequestObject *req = get_request (BaseUI::CallSlot);
+
+ if (req == 0) {
+ return;
+ }
+
+ req->the_slot = elSlot;
+ send_request (req);
+}
+
diff --git a/libs/pbd/pbd/abstract_ui.h b/libs/pbd/pbd/abstract_ui.h
index adb9aabf0e..daa1b83e3c 100644
--- a/libs/pbd/pbd/abstract_ui.h
+++ b/libs/pbd/pbd/abstract_ui.h
@@ -1,5 +1,5 @@
/*
- Copyright (C) 1998-99 Paul Barton-Davis
+ Copyright (C) 1998-2009 Paul Davis
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -34,38 +34,29 @@
class Touchable;
-template <class RequestObject>
+template<typename RequestObject>
class AbstractUI : public BaseUI
{
public:
- AbstractUI (std::string name, bool with_signal_pipe);
+ AbstractUI (const std::string& name);
virtual ~AbstractUI() {}
- virtual bool caller_is_ui_thread() = 0;
-
- void call_slot (sigc::slot<void> el_slot) {
- RequestObject *req = get_request (BaseUI::CallSlot);
-
- if (req == 0) {
- return;
- }
-
- req->slot = el_slot;
- send_request (req);
- }
-
- void register_thread (pthread_t, std::string);
- void register_thread_with_request_count (pthread_t, std::string, uint32_t num_requests);
+ void register_thread (std::string, pthread_t, std::string, uint32_t num_requests);
+ void call_slot (sigc::slot<void> el_slot);
protected:
typedef RingBufferNPT<RequestObject> RequestBuffer;
typedef typename RequestBuffer::rw_vector RequestBufferVector;
typedef typename std::map<pthread_t,RequestBuffer*>::iterator RequestBufferMapIterator;
-
- Glib::Mutex request_buffer_map_lock;
typedef std::map<pthread_t,RequestBuffer*> RequestBufferMap;
+
+ Glib::Mutex request_buffer_map_lock;
RequestBufferMap request_buffers;
- pthread_key_t thread_request_buffer_key;
+ Glib::Private<RequestBuffer> per_thread_request_buffer;
+
+ Glib::Mutex request_list_lock;
+ std::list<RequestObject*> request_list;
+
RequestObject* get_request (RequestType);
void handle_ui_requests ();
void send_request (RequestObject *);
diff --git a/libs/pbd/pbd/base_ui.h b/libs/pbd/pbd/base_ui.h
index 0928512841..614873e5d1 100644
--- a/libs/pbd/pbd/base_ui.h
+++ b/libs/pbd/pbd/base_ui.h
@@ -26,13 +26,22 @@
#include <sigc++/slot.h>
#include <sigc++/trackable.h>
+#include <glibmm/thread.h>
+#include <glibmm/main.h>
+
+#include "pbd/crossthread.h"
+
class BaseUI : virtual public sigc::trackable {
public:
- BaseUI (std::string name, bool with_signal_pipes);
+ BaseUI (const std::string& name);
virtual ~BaseUI();
BaseUI* base_instance() { return base_ui_instance; }
+ Glib::RefPtr<Glib::MainLoop> main_loop() const { return _main_loop; }
+ Glib::Thread* event_loop_thread() const { return run_loop_thread; }
+ bool caller_is_self () const { return Glib::Thread::self() == run_loop_thread; }
+
std::string name() const { return _name; }
bool ok() const { return _ok; }
@@ -49,17 +58,31 @@ class BaseUI : virtual public sigc::trackable {
static RequestType new_request_type();
static RequestType CallSlot;
+ void run ();
+ void quit ();
+
+ virtual void call_slot (sigc::slot<void> theSlot) = 0;
+
protected:
- int signal_pipe[2];
+ CrossThreadChannel request_channel;
bool _ok;
+ Glib::RefPtr<Glib::MainLoop> _main_loop;
+ Glib::Thread* run_loop_thread;
+
+ virtual void thread_init () {};
+ bool request_handler (Glib::IOCondition);
+
+ virtual void handle_ui_requests () = 0;
+
private:
std::string _name;
BaseUI* base_ui_instance;
+
+ static uint64_t rt_bit;
- static uint32_t rt_bit;
-
- int setup_signal_pipe ();
+ int setup_request_pipe ();
+ void main_thread ();
};
#endif /* __pbd_base_ui_h__ */
diff --git a/libs/pbd/pbd/crossthread.h b/libs/pbd/pbd/crossthread.h
index c63c863f38..f2fb4aa469 100644
--- a/libs/pbd/pbd/crossthread.h
+++ b/libs/pbd/pbd/crossthread.h
@@ -20,38 +20,25 @@
#ifndef __pbd__crossthread_h__
#define __pbd__crossthread_h__
-#include "pbd/abstract_ui.h"
-#include <sigc++/sigc++.h>
-#include <pthread.h>
-
-template<class RequestType>
-void
-call_slot_from_thread_or_dispatch_it (pthread_t thread_id, AbstractUI<RequestType>& ui, sigc::slot<void> theSlot)
-{
- /* when called, this function will determine whether the calling thread
- is the same as thread specified by the first argument. if it is,
- the we execute the slot. if not, we ask the interface given by the second
- argument to call the slot.
- */
-
- if (pthread_self() == thread_id) {
- theSlot ();
- } else {
- ui.call_slot (theSlot);
- }
-}
-
-template<class RequestType>
-sigc::slot<void>
-crossthread_safe (pthread_t thread_id, AbstractUI<RequestType>& ui, sigc::slot<void> theSlot)
-{
- /* this function returns a slot that will ensure that theSlot is either
- called by the specified thread or passed to the interface via
- AbstractUI::call_slot().
- */
-
- return sigc::bind (sigc::ptr_fun (call_slot_from_thread_or_dispatch_it<RequestType>),
- thread_id, ui, theSlot);
-}
+#include <glibmm/main.h>
+
+class CrossThreadChannel {
+ public:
+ CrossThreadChannel();
+ ~CrossThreadChannel();
+
+ void wakeup();
+ int selectable() const { return fds[0]; }
+
+ void drain ();
+ static void drain (int fd);
+
+ Glib::RefPtr<Glib::IOSource> ios();
+ bool ok() const { return fds[0] >= 0 && fds[1] >= 0; }
+
+ private:
+ Glib::RefPtr<Glib::IOSource> _ios; // lazily constructed
+ int fds[2];
+};
#endif /* __pbd__crossthread_h__ */
diff --git a/libs/pbd/pbd/pthread_utils.h b/libs/pbd/pbd/pthread_utils.h
index dd91e0a2b1..15b37662d5 100644
--- a/libs/pbd/pbd/pthread_utils.h
+++ b/libs/pbd/pbd/pthread_utils.h
@@ -27,19 +27,18 @@
#include <sigc++/sigc++.h>
-int pthread_create_and_store (std::string name, pthread_t *thread, pthread_attr_t *attr, void * (*start_routine)(void *), void * arg);
+int pthread_create_and_store (std::string name, pthread_t *thread, void * (*start_routine)(void *), void * arg);
void pthread_cancel_one (pthread_t thread);
void pthread_kill_all (int signum);
-void pthread_cancel_all ();
void pthread_exit_pbd (void* status);
std::string pthread_name ();
namespace PBD {
- extern void notify_gui_about_thread_creation (pthread_t, std::string, int requests = 256);
+ extern void notify_gui_about_thread_creation (std::string, pthread_t, std::string, int requests = 256);
extern void notify_gui_about_thread_exit (pthread_t);
- extern sigc::signal<void,pthread_t> ThreadLeaving;
- extern sigc::signal<void,pthread_t,std::string,uint32_t> ThreadCreatedWithRequestSize;
+ extern sigc::signal<void,pthread_t> ThreadLeaving;
+ extern sigc::signal<void,std::string,pthread_t,std::string,uint32_t> ThreadCreatedWithRequestSize;
}
#endif /* __pbd_pthread_utils__ */
diff --git a/libs/pbd/pthread_utils.cc b/libs/pbd/pthread_utils.cc
index 69270a767b..68082e6136 100644
--- a/libs/pbd/pthread_utils.cc
+++ b/libs/pbd/pthread_utils.cc
@@ -36,8 +36,8 @@ static pthread_mutex_t thread_map_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t gui_notify_lock = PTHREAD_MUTEX_INITIALIZER;
namespace PBD {
- sigc::signal<void,pthread_t> ThreadLeaving;
- sigc::signal<void,pthread_t,std::string,uint32_t> ThreadCreatedWithRequestSize;
+ sigc::signal<void,pthread_t> ThreadLeaving;
+ sigc::signal<void,std::string, pthread_t,std::string,uint32_t> ThreadCreatedWithRequestSize;
}
using namespace PBD;
@@ -52,10 +52,10 @@ static int thread_creator (pthread_t* thread_id, const pthread_attr_t* attr, voi
}
void
-PBD::notify_gui_about_thread_creation (pthread_t thread, std::string str, int request_count)
+PBD::notify_gui_about_thread_creation (std::string target_gui, pthread_t thread, std::string str, int request_count)
{
pthread_mutex_lock (&gui_notify_lock);
- ThreadCreatedWithRequestSize (thread, str, request_count);
+ ThreadCreatedWithRequestSize (target_gui, thread, str, request_count);
pthread_mutex_unlock (&gui_notify_lock);
}
@@ -68,35 +68,27 @@ PBD::notify_gui_about_thread_exit (pthread_t thread)
}
int
-pthread_create_and_store (string name, pthread_t *thread, pthread_attr_t *attr, void * (*start_routine)(void *), void * arg)
+pthread_create_and_store (string name, pthread_t *thread, void * (*start_routine)(void *), void * arg)
{
- int ret;
-
pthread_attr_t default_attr;
- bool use_default_attr = (attr == NULL);
+ int ret;
- if (use_default_attr) {
- // set default stack size to sensible default for memlocking
- pthread_attr_init(&default_attr);
- pthread_attr_setstacksize(&default_attr, 500000);
- attr = &default_attr;
- }
+ // set default stack size to sensible default for memlocking
+ pthread_attr_init(&default_attr);
+ pthread_attr_setstacksize(&default_attr, 500000);
- if ((ret = thread_creator (thread, attr, start_routine, arg)) == 0) {
+ if ((ret = thread_creator (thread, &default_attr, start_routine, arg)) == 0) {
std::pair<string,pthread_t> newpair;
newpair.first = name;
newpair.second = *thread;
pthread_mutex_lock (&thread_map_lock);
all_threads.insert (newpair);
-
pthread_mutex_unlock (&thread_map_lock);
}
- if (use_default_attr) {
- pthread_attr_destroy(&default_attr);
- }
-
+ pthread_attr_destroy(&default_attr);
+
return ret;
}
@@ -132,19 +124,6 @@ pthread_kill_all (int signum)
}
void
-pthread_cancel_all ()
-{
- pthread_mutex_lock (&thread_map_lock);
- for (ThreadMap::iterator i = all_threads.begin(); i != all_threads.end(); ++i) {
- if (i->second != pthread_self()) {
- pthread_cancel (i->second);
- }
- }
- all_threads.clear();
- pthread_mutex_unlock (&thread_map_lock);
-}
-
-void
pthread_cancel_one (pthread_t thread)
{
pthread_mutex_lock (&thread_map_lock);
diff --git a/libs/pbd/wscript b/libs/pbd/wscript
index 122f38da79..e114cbe1ae 100644
--- a/libs/pbd/wscript
+++ b/libs/pbd/wscript
@@ -57,6 +57,7 @@ def build(bld):
command.cc
convert.cc
controllable.cc
+ crossthread.cc
enumwriter.cc
dmalloc.cc
error.cc