carb/BindingsUtils.h

File members: carb/BindingsUtils.h

// Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto. Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//

#pragma once

#include "ClientUtils.h"
#include "Defines.h"
#include "Format.h"
#include "Framework.h"
#include "InterfaceUtils.h"
#include "ObjectUtils.h"
#include "assert/AssertUtils.h"
#include "logging/Log.h"
#include "profiler/Profile.h"

#include <functional>
#include <sstream>
#include <string>
#include <unordered_map>

namespace carb
{

template <typename InterfaceType, typename ReturnType, typename... Args>
auto wrapInterfaceFunction(ReturnType (*InterfaceType::*p)(Args...))
    -> std::function<ReturnType(InterfaceType&, Args...)>
{
    return [p](InterfaceType& c, Args... args) { return (c.*p)(args...); };
}

template <typename InterfaceType, typename ReturnType, typename... Args>
auto wrapInterfaceFunction(const InterfaceType* c, ReturnType (*InterfaceType::*p)(Args...))
    -> std::function<ReturnType(Args...)>
{
    return [c, p](Args... args) { return (c->*p)(args...); };
}

template <typename InterfaceType>
InterfaceType* acquireInterfaceForBindings(const char* pluginName = nullptr)
{
    carb::Framework* framework = carb::getFramework();
    InterfaceType* iface = framework->tryAcquireInterface<InterfaceType>(pluginName);
    if (!iface)
    {
        // Try load plugins with default desc (all of them)
        carb::PluginLoadingDesc desc = carb::PluginLoadingDesc::getDefault();
        framework->loadPlugins(desc);
        iface = framework->tryAcquireInterface<InterfaceType>(pluginName);
        if (!iface)
        {
            // somehow this header gets picked up by code compiled by -fno-exceptions
#if !CARB_EXCEPTIONS_ENABLED
            OMNI_FATAL_UNLESS(iface, "Failed to acquire interface: '%s' (pluginName: '%s')",
                              InterfaceType::getInterfaceDesc().name, pluginName ? pluginName : "nullptr");
#else
            throw std::runtime_error(fmt::format("Failed to acquire interface: {} (pluginName: {})",
                                                 InterfaceType::getInterfaceDesc().name,
                                                 pluginName ? pluginName : "nullptr"));
#endif
        }
    }
    return iface;
}

template <typename InterfaceType>
InterfaceType* getCachedInterfaceForBindings()
{
    InterfaceType* iface = carb::getCachedInterface<InterfaceType>();
    CARB_UNLIKELY_IF(!iface)
    {
        // somehow this header gets picked up by code compiled by -fno-exceptions
#if !CARB_EXCEPTIONS_ENABLED
        OMNI_FATAL_UNLESS(iface, "Failed to acquire cached interface: '%s'", InterfaceType::getInterfaceDesc().name);
#else
        throw std::runtime_error(
            fmt::format("Failed to acquire cached interface: {}", InterfaceType::getInterfaceDesc().name));
#endif
    }
    return iface;
}

template <typename InterfaceType>
InterfaceType* acquireInterfaceFromLibraryForBindings(const char* libraryPath)
{
    carb::Framework* framework = carb::getFramework();
    InterfaceType* iface = framework->tryAcquireInterfaceFromLibrary<InterfaceType>(libraryPath);
    if (!iface)
    {
        // somehow this header gets picked up by code compiled by -fno-exceptions
#if !CARB_EXCEPTIONS_ENABLED
        OMNI_FATAL_UNLESS(
            "Failed to acquire interface: '%s' from: '%s')", InterfaceType::getInterfaceDesc().name, libraryPath);
#else
        throw std::runtime_error(fmt::format(
            "Failed to acquire interface: {} from: {})", InterfaceType::getInterfaceDesc().name, libraryPath));
#endif
    }
    return iface;
}

inline Framework* acquireFrameworkForBindings(const char* scriptLanguage)
{
    // Acquire framework and set into global variable
    // Is framework was previously invalid, we are the first who calling it and it will be created during acquire.
    // Register builtin plugin in that case
    const bool firstStart = !isFrameworkValid();
    Framework* f = acquireFramework(g_carbClientName);
    if (!f)
        return nullptr;
    g_carbFramework = f;

    // Register as binding for the given script language
    f->registerScriptBinding(BindingType::Binding, g_carbClientName, scriptLanguage);

    // Starting up logging
    if (firstStart)
        detail::registerBuiltinLogging(f);
    logging::registerLoggingForClient();

    // Starting up filesystem and profiling
    if (firstStart)
    {
        detail::registerBuiltinFileSystem(f);
        detail::registerBuiltinAssert(f);
        detail::registerBuiltinThreadUtil(f);
    }
    profiler::registerProfilerForClient();
    assert::registerAssertForClient();
    l10n::registerLocalizationForClient();
    return f;
}

inline void releaseFrameworkForBindings()
{
    if (isFrameworkValid())
    {
        profiler::deregisterProfilerForClient();
        logging::deregisterLoggingForClient();
        assert::deregisterAssertForClient();
        l10n::deregisterLocalizationForClient();

        // Leave g_carbFramework intact here since the framework itself remains valid; we are just signaling our end of
        // using it. There may be some static destructors (i.e. CachedInterface) that still need to use it.
    }
    else
    {
        // The framework became invalid while we were loaded.
        g_carbFramework = nullptr;
    }
}

class FrameworkInitializerForBindings
{
public:
    FrameworkInitializerForBindings(const char* scriptLanguage = "python")
    {
        acquireFrameworkForBindings(scriptLanguage);

        m_thisModuleStartedOmniCore = !omniGetTypeFactoryWithoutAcquire();
        if (m_thisModuleStartedOmniCore)
        {
            // at this point, the core should already be started by the omniverse host executable (i.e. app).  however,
            // if we're in the python native interpreter, it will not automatically startup the core.  here we account
            // for this situation by checking if the core is started, and if not, start it.
            //
            // OMNI_CORE_START internally reference counts the start/stop calls, so one would think we could always make
            // this call (with a corresponding call to OMNI_CORE_STOP in the destructor).
            //
            // however, the Python interpreter doesn't like unloading .pyd files, meaning our destructor will not be
            // called.
            //
            // this shouldn't be an issue, unless the host expects to be able to load, unload, and then reload the core.
            // the result here would be the internal reference count would get confused, causing the core to never be
            // unloaded.
            //
            // we don't expect apps to reload the core, but our unit tests do.  so, here we only let python increment
            // the ref count if we think its the first entity to start the core (i.e. running in the interpreter).
            OMNI_CORE_START(nullptr);
        }

        omni::structuredlog::addModulesSchemas();
    }

    ~FrameworkInitializerForBindings()
    {
        if (m_thisModuleStartedOmniCore)
        {
            OMNI_CORE_STOP_FOR_BINDINGS();
            m_thisModuleStartedOmniCore = false;
        }
        releaseFrameworkForBindings();
    }

    bool m_thisModuleStartedOmniCore;
};

template <class T1, class T2>
inline size_t hashPair(T1 t1, T2 t2)
{
    std::size_t res = 0;
    using std::hash;
    res = carb::hashCombine(res, hash<T1>{}(t1));
    res = carb::hashCombine(res, hash<T2>{}(t2));
    return res;
}

#ifndef DOXYGEN_SHOULD_SKIP_THIS
template <class KeyT, typename ReturnT, typename... Args>
class ScriptCallbackRegistry
{
public:
    using FuncT = std::function<ReturnT(Args...)>;

    static FuncT* create(const FuncT& f)
    {
        return new FuncT(f);
    }

    static void destroy(FuncT* f)
    {
        delete f;
    }

    void add(const KeyT& key, FuncT* ptr)
    {
        if (!m_map.insert({ key, ptr }).second)
        {
            CARB_LOG_ERROR("Scripting callback with that key already exists.");
        }
    }

    bool tryRemoveAndDestroy(const KeyT& key)
    {
        auto it = m_map.find(key);
        if (it != m_map.end())
        {
            destroy(it->second);
            m_map.erase(it);
            return true;
        }
        return false;
    }

    void removeAndDestroy(const KeyT& key)
    {
        if (!tryRemoveAndDestroy(key))
        {
            CARB_LOG_ERROR("Removing unknown scripting callback.");
        }
    }

private:
    std::unordered_map<KeyT, FuncT*> m_map;
};

template <typename ClassT, typename ObjectT, typename... Args>
auto wrapInStealObject(ObjectT* (ClassT::*f)(Args...))
{
    return [f](ClassT* c, Args... args) { return carb::stealObject<ObjectT>((c->*f)(args...)); };
}
#endif

} // namespace carb

#define CARB_BINDINGS(clientName, ...)                                                                                 \
    CARB_GLOBALS(clientName)                                                                                           \
    carb::FrameworkInitializerForBindings g_carbFrameworkInitializerForBindings{ __VA_ARGS__ };

#define CARB_BINDINGS_EX(clientName_, desc_, ...)                                                                      \
    CARB_GLOBALS_EX(clientName_, desc_)                                                                                \
    carb::FrameworkInitializerForBindings g_carbFrameworkInitializerForBindings{ __VA_ARGS__ };