ice/turn transport: make sure PJSIP is initialized before it's used

Using PJSIP functions without calling pj_init first can lead to subtle
non-deterministic bugs.

GitLab: #18
Change-Id: I9364fd247165c0ce19a8d0d42575fb66651b54a3
diff --git a/include/ice_transport_factory.h b/include/ice_transport_factory.h
index df3367c..3e4c931 100644
--- a/include/ice_transport_factory.h
+++ b/include/ice_transport_factory.h
@@ -19,6 +19,7 @@
 #include "ice_options.h"
 #include "ice_transport.h"
 #include "ip_utils.h"
+#include "pj_init_lock.h"
 
 #include <functional>
 #include <memory>
@@ -50,6 +51,9 @@
     std::shared_ptr<pj_caching_pool> getPoolCaching() { return cp_; }
 
 private:
+    // Declaring pjInitLock_ before cp_ because its constructor needs to be called
+    // first (see constructor implementation for a comment with more information).
+    PjInitLock pjInitLock_;
     std::shared_ptr<pj_caching_pool> cp_;
     pj_ice_strans_cfg ice_cfg_;
     std::shared_ptr<Logger> logger_ {};
diff --git a/include/pj_init_lock.h b/include/pj_init_lock.h
new file mode 100644
index 0000000..c521a8a
--- /dev/null
+++ b/include/pj_init_lock.h
@@ -0,0 +1,59 @@
+/*
+ *  Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ *  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 3 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, see <https://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <fmt/core.h>
+#include <mutex>
+#include <pj/errno.h>
+#include <pj/types.h>
+
+namespace dhtnet {
+
+// PJSIP expects the number of calls to pj_shutdown to match the number of calls
+// to pj_init (https://docs.pjsip.org/en/latest/specific-guides/develop/init_shutdown_thread.html).
+// The intended behavior seems to be the following:
+// - The first call to pj_init actually initializes the library; subsequent calls do nothing.
+// - All calls to pj_shutdown do nothing, except the last one which actually performs the shutdown.
+// Unfortunately, the way this logic is implemented in PJSIP is not thread-safe, so we're
+// responsible for making sure that these functions can't be called by two threads at the same time.
+class PjInitLock
+{
+private:
+    inline static std::mutex mutex_;
+
+public:
+    PjInitLock()
+    {
+        std::lock_guard lk(mutex_);
+        pj_status_t status = pj_init();
+
+        if (status != PJ_SUCCESS) {
+            char errorMessage[PJ_ERR_MSG_SIZE];
+            pj_strerror(status, errorMessage, sizeof(errorMessage));
+            throw std::runtime_error(
+                fmt::format("pj_init failed: {}", errorMessage));
+        }
+    }
+
+    ~PjInitLock()
+    {
+        std::lock_guard lk(mutex_);
+        pj_shutdown();
+    }
+};
+
+}