Refactor global object tables logic.
This unifies the implementation for plugins and resource providers. PiperOrigin-RevId: 605617372 Change-Id: I2ea52b4c53f48c4c59edd5fdc1c51c7edba7340e
This commit is contained in:
committed by
Copybara-Service
parent
6fa139a693
commit
dc40bbd1f5
@@ -37,6 +37,7 @@ set(MUJOCO_ENGINE_SRCS
|
||||
engine_derivative_fd.h
|
||||
engine_forward.c
|
||||
engine_forward.h
|
||||
engine_global_table.h
|
||||
engine_inverse.c
|
||||
engine_inverse.h
|
||||
engine_island.c
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
// Copyright 2024 DeepMind Technologies Limited
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef MUJOCO_SRC_ENGINE_ENGINE_GLOBAL_TABLE_H_
|
||||
#define MUJOCO_SRC_ENGINE_ENGINE_GLOBAL_TABLE_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
#include "engine/engine_util_errmem.h"
|
||||
|
||||
namespace mujoco {
|
||||
static constexpr int kCacheLineBytes = 256;
|
||||
|
||||
static inline bool CaseInsensitiveEqual(std::string_view s1, std::string_view s2) {
|
||||
if (s1.length() != s2.length()) {
|
||||
return false;
|
||||
}
|
||||
auto len = s1.length();
|
||||
for (decltype(len) i = 0; i < len; ++i) {
|
||||
if (std::tolower(s1[i]) != std::tolower(s2[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// A table intended for use as global storage for extension objects such as plugins, implemented as
|
||||
// a linked list of array "blocks". This is a compromise that maintains a good degree of memory
|
||||
// locality while not invalidating existing pointers when growing the table. It is expected that for
|
||||
// most users, the number of objects loaded will be small enough to fit in the initial block, and
|
||||
// so the global table will behave like an array. Since pointers are never invalidated, we do not
|
||||
// need to apply a read lock on the global table when resolving an element.
|
||||
template<typename T>
|
||||
struct alignas(kCacheLineBytes) TableBlock {
|
||||
static constexpr int kBlockSize = 15;
|
||||
|
||||
TableBlock() : objects{}, next(nullptr) {
|
||||
static_assert(
|
||||
sizeof(TableBlock<T>) / kCacheLineBytes ==
|
||||
sizeof(TableBlock<T>::objects) / kCacheLineBytes
|
||||
+ (sizeof(TableBlock<T>::objects) % kCacheLineBytes > 0),
|
||||
"TableBlock::next doesn't fit in the same cache line as the end of TableBlock::objects");
|
||||
}
|
||||
|
||||
T objects[kBlockSize];
|
||||
TableBlock<T>* next;
|
||||
};
|
||||
|
||||
|
||||
using Mutex = std::mutex;
|
||||
|
||||
class ReentrantWriteLock {
|
||||
public:
|
||||
ReentrantWriteLock(Mutex& mutex) : mutex_(mutex) {
|
||||
if (LockCountOnCurrentThread() == 0) {
|
||||
mutex_.lock();
|
||||
}
|
||||
++LockCountOnCurrentThread();
|
||||
}
|
||||
|
||||
~ReentrantWriteLock() {
|
||||
if (--LockCountOnCurrentThread() == 0) {
|
||||
mutex_.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Mutex& mutex_;
|
||||
|
||||
static int& LockCountOnCurrentThread() noexcept {
|
||||
thread_local int counter = 0;
|
||||
return counter;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
class GlobalTable {
|
||||
public:
|
||||
static GlobalTable<T>& GetSingleton() {
|
||||
static_assert(std::is_trivially_destructible_v<GlobalTable<T>>);
|
||||
static GlobalTable<T> global;
|
||||
return global;
|
||||
}
|
||||
|
||||
// Each extension object type T must implement these.
|
||||
using ErrorMessage = char[512];
|
||||
static const char* HumanReadableTypeName();
|
||||
static std::string_view ObjectKey(const T&);
|
||||
static bool ObjectEqual(const T&, const T&);
|
||||
static bool CopyObject(T& dst, const T& src, ErrorMessage& err);
|
||||
|
||||
int count() {
|
||||
return count_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
ReentrantWriteLock LockExclusively() {
|
||||
return ReentrantWriteLock(mutex());
|
||||
}
|
||||
|
||||
int AppendIfUnique(const T& obj) {
|
||||
ErrorMessage err = "\0";
|
||||
|
||||
// ========= ATTENTION! ========================================================================
|
||||
// Do not handle objects with nontrivial destructors outside of this lambda.
|
||||
// Do not call mju_error inside this lambda.
|
||||
int slot = [&]() {
|
||||
auto lock = LockExclusively();
|
||||
|
||||
int count = count_.load(std::memory_order_acquire);
|
||||
int local_idx = 0;
|
||||
TableBlock<T>* block = &first_block_;
|
||||
|
||||
// check if a non-identical object has already been registered
|
||||
for (int i = 0; i < count; ++i, ++local_idx) {
|
||||
if (local_idx == TableBlock<T>::kBlockSize) {
|
||||
local_idx = 0;
|
||||
block = block->next;
|
||||
}
|
||||
const T& existing = block->objects[local_idx];
|
||||
if (CaseInsensitiveEqual(ObjectKey(obj), ObjectKey(existing))) {
|
||||
if (!ObjectEqual(obj, existing)) {
|
||||
std::snprintf(err, sizeof(err), "%s '%s' is already registered",
|
||||
HumanReadableTypeName(), std::string(ObjectKey(obj)).c_str());
|
||||
return -1;
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// allocate a new block if the last allocated block is full
|
||||
if (local_idx == TableBlock<T>::kBlockSize) {
|
||||
local_idx = 0;
|
||||
block->next = new(std::nothrow) TableBlock<T>;
|
||||
if (!block->next) {
|
||||
std::snprintf(err, sizeof(err), "failed to allocate memory for a new %s table block",
|
||||
HumanReadableTypeName());
|
||||
return -1;
|
||||
}
|
||||
block = block->next;
|
||||
}
|
||||
|
||||
// copy the new object into the table
|
||||
if (!CopyObject(block->objects[local_idx], obj, err)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// increment the global count with a release memory barrier
|
||||
count_.store(count + 1, std::memory_order_release);
|
||||
|
||||
return count;
|
||||
}();
|
||||
// ========= ATTENTION! ========================================================================
|
||||
// End of safe lambda, do not handle objects with non-trivial destructors beyond this point.
|
||||
|
||||
// registration failed, throw an mju_error
|
||||
if (slot < 0) {
|
||||
err[sizeof(err) - 1] = '\0';
|
||||
mju_error("%s", err);
|
||||
}
|
||||
|
||||
return slot;
|
||||
}
|
||||
|
||||
// look up by slot number, assuming that count() has already been called
|
||||
const T* GetAtSlotUnsafe(int slot, int nslot) {
|
||||
if (slot < 0 || slot >= nslot) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TableBlock<T>* block = &first_block_;
|
||||
|
||||
// iterate over blocks in the global table until the local index is less than the block size
|
||||
int local_idx = slot;
|
||||
while (local_idx >= TableBlock<T>::kBlockSize) {
|
||||
local_idx -= TableBlock<T>::kBlockSize;
|
||||
block = block->next;
|
||||
if (!block) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// local_idx is now a valid index into the current block
|
||||
T* obj = &(block->objects[local_idx]);
|
||||
|
||||
// check if obj has been initialized
|
||||
if (obj && ObjectKey(*obj).empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
// look up by key, assuming that count() has already been called
|
||||
const T* GetByKeyUnsafe(std::string_view key, int* slot, int nslot) {
|
||||
if (slot) *slot = -1;
|
||||
|
||||
if (key.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TableBlock<T>* block = &first_block_;
|
||||
int found_slot = 0;
|
||||
while (block) {
|
||||
for (int i = 0;
|
||||
i < TableBlock<T>::kBlockSize && found_slot < nslot;
|
||||
++i, ++found_slot) {
|
||||
const T& obj = block->objects[i];
|
||||
|
||||
// reached an uninitialized object, which means that iterated beyond the object count
|
||||
// this should never happen if `count` was actually returned by count()
|
||||
std::string_view candidate_key = ObjectKey(obj);
|
||||
if (candidate_key.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// check if key matches the query
|
||||
if (CaseInsensitiveEqual(candidate_key, key)) {
|
||||
if (slot) *slot = found_slot;
|
||||
return &obj;
|
||||
}
|
||||
}
|
||||
|
||||
block = block->next;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const T* GetAtSlot(int slot) {
|
||||
// count() uses memory_order_acquire which acts as a barrier that guarantees that all
|
||||
// objects up to `count` have been completely inserted
|
||||
return GetAtSlotUnsafe(slot, count());
|
||||
}
|
||||
|
||||
const T* GetByKey(std::string_view key, int* slot) {
|
||||
// count() uses memory_order_acquire which acts as a barrier that guarantees that all
|
||||
// objects up to `count` have been completely inserted
|
||||
return GetByKeyUnsafe(key, slot, count());
|
||||
}
|
||||
|
||||
private:
|
||||
GlobalTable() {
|
||||
new(mutex_) Mutex;
|
||||
}
|
||||
|
||||
Mutex& mutex() {
|
||||
return *std::launder(reinterpret_cast<Mutex*>(&mutex_));
|
||||
}
|
||||
|
||||
TableBlock<T> first_block_;
|
||||
std::atomic_int count_;
|
||||
|
||||
// A mutex whose destructor is never run.
|
||||
// When a C++ program terminates, the destructors for function static objects and globals will be
|
||||
// executed by whichever thread started that termination but there is no guarantee that other
|
||||
// threads have terminated. In other words, a static object may be accessed by another thread
|
||||
// after it is deleted. We avoid destruction issues by never running the destructor.
|
||||
alignas(Mutex) unsigned char mutex_[sizeof(Mutex)];
|
||||
};
|
||||
|
||||
} // namespace mujoco
|
||||
|
||||
#endif // MUJOCO_SRC_ENGINE_ENGINE_GLOBAL_TABLE_H_
|
||||
+176
-481
@@ -20,20 +20,13 @@
|
||||
|
||||
#include "engine/engine_plugin.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <string_view>
|
||||
|
||||
extern "C" {
|
||||
#if defined(_WIN32) || defined(__CYGWIN__)
|
||||
@@ -45,6 +38,7 @@ extern "C" {
|
||||
}
|
||||
|
||||
#include <mujoco/mjplugin.h>
|
||||
#include "engine/engine_global_table.h"
|
||||
#include "engine/engine_util_errmem.h"
|
||||
|
||||
// set default plugin definition
|
||||
@@ -53,139 +47,11 @@ void mjp_defaultPlugin(mjpPlugin* plugin) {
|
||||
}
|
||||
|
||||
namespace {
|
||||
using mujoco::GlobalTable;
|
||||
|
||||
constexpr int kMaxNameLength = 1024;
|
||||
constexpr int kMaxAttributes = 255;
|
||||
|
||||
constexpr int kCacheLine = 256;
|
||||
|
||||
// A table of registered plugins, implemented as a linked list of array "blocks".
|
||||
// This is a compromise that maintains a good degree of memory locality while not invalidating
|
||||
// existing pointers when growing the table. It is expected that for most users, the number of
|
||||
// plugins loaded into a program will be small enough to fit in the initial block, and so the global
|
||||
// table will behave like an array. Since pointers are never invalidated, we do not need to apply a
|
||||
// read lock on the global table when resolving a plugin.
|
||||
template<typename T>
|
||||
struct alignas(kCacheLine) PluginTable {
|
||||
static constexpr int kBlockSize = 15;
|
||||
|
||||
PluginTable() {
|
||||
for (int i = 0; i < kBlockSize; ++i) {
|
||||
std::memset(&plugins[i], 0, sizeof(plugins[i]));
|
||||
}
|
||||
}
|
||||
|
||||
T plugins[kBlockSize];
|
||||
PluginTable<T>* next = nullptr;
|
||||
};
|
||||
|
||||
static_assert(
|
||||
sizeof(PluginTable<mjpPlugin>) / kCacheLine ==
|
||||
sizeof(PluginTable<mjpPlugin>::plugins) / kCacheLine
|
||||
+ (sizeof(PluginTable<mjpPlugin>::plugins) % kCacheLine > 0),
|
||||
"PluginTable::next doesn't fit in the same cache line as the end of PluginTable::plugins");
|
||||
|
||||
using Mutex = std::shared_mutex;
|
||||
|
||||
class ReentrantWriteLock {
|
||||
public:
|
||||
ReentrantWriteLock(Mutex& mutex) : mutex_(mutex) {
|
||||
if (LockCountOnCurrentThread() == 0) {
|
||||
mutex_.lock();
|
||||
}
|
||||
++LockCountOnCurrentThread();
|
||||
}
|
||||
|
||||
~ReentrantWriteLock() {
|
||||
if (--LockCountOnCurrentThread() == 0) {
|
||||
mutex_.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Mutex& mutex_;
|
||||
|
||||
static int& LockCountOnCurrentThread() noexcept {
|
||||
thread_local int counter = 0;
|
||||
return counter;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
class Global {
|
||||
public:
|
||||
Global() {
|
||||
new(mutex_) Mutex;
|
||||
}
|
||||
PluginTable<T>& table() {
|
||||
return table_;
|
||||
}
|
||||
std::atomic_int& count() {
|
||||
return count_;
|
||||
}
|
||||
Mutex& mutex() {
|
||||
return *std::launder(reinterpret_cast<Mutex*>(&mutex_));
|
||||
}
|
||||
|
||||
ReentrantWriteLock lock_mutex_exclusively() {
|
||||
return ReentrantWriteLock(mutex());
|
||||
}
|
||||
|
||||
private:
|
||||
PluginTable<T> table_;
|
||||
std::atomic_int count_;
|
||||
|
||||
// A mutex whose destructor is never run.
|
||||
// When a C++ program terminates, the destructors for function static objects and globals will be
|
||||
// executed by whichever thread started that termination but there is no guarantee that other
|
||||
// threads have terminated. In other words, a static object may be accessed by another thread
|
||||
// after it is deleted. We avoid destruction issues by never running the destructor.
|
||||
alignas(Mutex) unsigned char mutex_[sizeof(Mutex)];
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
Global<T>& GetGlobal() {
|
||||
static Global<T> global;
|
||||
static_assert(std::is_trivially_destructible_v<decltype(global)>);
|
||||
return global;
|
||||
}
|
||||
|
||||
// allocate new block for the global table
|
||||
template<typename T>
|
||||
PluginTable<T>* AddNewTableBlock(PluginTable<T>* table) {
|
||||
char err[512];
|
||||
err[0] = '\0';
|
||||
table->next = new(std::nothrow) PluginTable<T>;
|
||||
if (!table->next) {
|
||||
std::snprintf(err, sizeof(err), "failed to allocate memory for the global plugin table");
|
||||
return nullptr;
|
||||
}
|
||||
return table->next;
|
||||
}
|
||||
|
||||
// look up a plugin by slot number
|
||||
template<typename T>
|
||||
const T* GetAtSlot(int slot, int nslot) {
|
||||
if (slot < 0 || slot >= nslot) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Global<T>& global = GetGlobal<T>();
|
||||
PluginTable<T>* table = &global.table();
|
||||
|
||||
// iterate over blocks in the global table until the local index is less than the block size
|
||||
int local_idx = slot;
|
||||
while (local_idx >= PluginTable<T>::kBlockSize) {
|
||||
local_idx -= PluginTable<T>::kBlockSize;
|
||||
table = table->next;
|
||||
if (!table) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// local_idx is now a valid index into the current block
|
||||
return &(table->plugins[local_idx]);
|
||||
}
|
||||
|
||||
// return the length of a null-terminated string, or -1 if it is not terminated after kMaxNameLength
|
||||
int strklen(const char* s) {
|
||||
for (int i = 0; i < kMaxNameLength; ++i) {
|
||||
@@ -211,68 +77,6 @@ std::unique_ptr<char[]> CopyName(const char* s) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// check if two plugins are identical
|
||||
bool PluginsAreIdentical(const mjpPlugin& plugin1, const mjpPlugin& plugin2) {
|
||||
if (plugin1.name && !plugin2.name) {
|
||||
return false;
|
||||
}
|
||||
if (plugin2.name && !plugin1.name) {
|
||||
return false;
|
||||
}
|
||||
if (plugin1.name && plugin2.name &&
|
||||
std::strncmp(plugin1.name, plugin2.name, kMaxNameLength)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (plugin1.nattribute != plugin2.nattribute) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < plugin1.nattribute; ++i) {
|
||||
if (plugin1.attributes[i] && !plugin2.attributes[i]) {
|
||||
return false;
|
||||
}
|
||||
if (plugin2.attributes[i] && !plugin1.attributes[i]) {
|
||||
return false;
|
||||
}
|
||||
if (plugin1.attributes[i] && plugin2.attributes[i] &&
|
||||
std::strncmp(plugin1.attributes[i], plugin2.attributes[i],
|
||||
kMaxNameLength)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const char* ptr1 = reinterpret_cast<const char*>(&plugin1.attributes) +
|
||||
sizeof(plugin1.attributes);
|
||||
const char* ptr2 = reinterpret_cast<const char*>(&plugin2.attributes) +
|
||||
sizeof(plugin2.attributes);
|
||||
std::size_t remaining_size =
|
||||
sizeof(mjpPlugin) - (ptr1 - reinterpret_cast<const char*>(&plugin1));
|
||||
return !std::memcmp(ptr1, ptr2, remaining_size);
|
||||
}
|
||||
|
||||
// does case insensitive comparison
|
||||
bool PrefixesAreIdentical(const char* p1, const char* p2) {
|
||||
int i = 0;
|
||||
for (; p1[i] != '\0'; i++) {
|
||||
if (std::tolower(p1[i]) != std::tolower(p2[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return p2[i] == '\0';
|
||||
}
|
||||
|
||||
// check if two resource providers are identical
|
||||
bool ResourceProvidersAreIdentical(const mjpResourceProvider* p1, const mjpResourceProvider* p2) {
|
||||
return (PrefixesAreIdentical(p1->prefix, p2->prefix) &&
|
||||
p1->open == p2->open &&
|
||||
p1->read == p2->read &&
|
||||
p1->close == p2->close &&
|
||||
p1->getdir == p2->getdir &&
|
||||
p1->modified == p2->modified &&
|
||||
p1->data == p2->data);
|
||||
}
|
||||
|
||||
// check if prefix is a valid URI scheme format
|
||||
bool IsValidURISchemeFormat(const char* prefix) {
|
||||
int len;
|
||||
@@ -299,8 +103,168 @@ bool IsValidURISchemeFormat(const char* prefix) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// seek the nth config attrib of a plugin instance by counting null terminators
|
||||
const char* PluginAttrSeek(const mjModel* m, int plugin_id, int attrib_id) {
|
||||
const char* ptr = m->plugin_attr + m->plugin_attradr[plugin_id];
|
||||
for (int i = 0; i < attrib_id; ++i) {
|
||||
while (*ptr) {
|
||||
++ptr;
|
||||
}
|
||||
++ptr;
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
template <>
|
||||
const char* GlobalTable<mjpPlugin>::HumanReadableTypeName() {
|
||||
return "plugin";
|
||||
}
|
||||
|
||||
template <>
|
||||
std::string_view GlobalTable<mjpPlugin>::ObjectKey(const mjpPlugin& plugin) {
|
||||
return std::string_view(plugin.name, strklen(plugin.name));
|
||||
}
|
||||
|
||||
// check if two plugins are identical
|
||||
template <>
|
||||
bool GlobalTable<mjpPlugin>::ObjectEqual(const mjpPlugin& plugin1, const mjpPlugin& plugin2) {
|
||||
if (plugin1.name && !plugin2.name) {
|
||||
return false;
|
||||
}
|
||||
if (plugin2.name && !plugin1.name) {
|
||||
return false;
|
||||
}
|
||||
if (plugin1.name && plugin2.name &&
|
||||
std::strncmp(plugin1.name, plugin2.name, kMaxNameLength)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (plugin1.nattribute != plugin2.nattribute) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < plugin1.nattribute; ++i) {
|
||||
if (plugin1.attributes[i] && !plugin2.attributes[i]) {
|
||||
return false;
|
||||
}
|
||||
if (plugin1.attributes[i] && !plugin2.attributes[i]) {
|
||||
return false;
|
||||
}
|
||||
if (plugin1.attributes[i] && plugin2.attributes[i] &&
|
||||
std::strncmp(plugin1.attributes[i], plugin2.attributes[i],
|
||||
kMaxNameLength)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const char* ptr1 = reinterpret_cast<const char*>(&plugin1.attributes) +
|
||||
sizeof(plugin1.attributes);
|
||||
const char* ptr2 = reinterpret_cast<const char*>(&plugin2.attributes) +
|
||||
sizeof(plugin2.attributes);
|
||||
std::size_t remaining_size =
|
||||
sizeof(mjpPlugin) - (ptr1 - reinterpret_cast<const char*>(&plugin1));
|
||||
return !std::memcmp(ptr1, ptr2, remaining_size);
|
||||
}
|
||||
|
||||
template <>
|
||||
bool GlobalTable<mjpPlugin>::CopyObject(mjpPlugin& dst, const mjpPlugin& src, ErrorMessage& err) {
|
||||
// check and copy the plugin name
|
||||
std::unique_ptr<char[]> name = CopyName(src.name);
|
||||
if (!name) {
|
||||
if (strklen(src.name) == -1) {
|
||||
std::snprintf(err, sizeof(err),
|
||||
"plugin->name length exceeds the maximum limit of %d", kMaxNameLength);
|
||||
} else {
|
||||
std::snprintf(err, sizeof(err), "failed to allocate memory for plugin name");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// check and copy plugin attributes
|
||||
std::unique_ptr<std::unique_ptr<char[]>[]> attributes_list;
|
||||
if (src.nattribute) {
|
||||
attributes_list.reset(new(std::nothrow) std::unique_ptr<char[]>[src.nattribute]);
|
||||
if (!attributes_list) {
|
||||
std::snprintf(err, sizeof(err), "failed to allocate memory for plugin attribute list");
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < src.nattribute; ++i) {
|
||||
std::unique_ptr<char[]> attr = CopyName(src.attributes[i]);
|
||||
if (!attr) {
|
||||
if (strklen(src.attributes[i]) == -1) {
|
||||
std::snprintf(
|
||||
err, sizeof(err),
|
||||
"length of plugin attribute %d exceeds the maximum limit of %d", i, kMaxAttributes);
|
||||
} else {
|
||||
std::snprintf(err, sizeof(err), "failed to allocate memory for plugin attribute %d", i);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
attributes_list[i].swap(attr);
|
||||
}
|
||||
}
|
||||
|
||||
// release the attribute names from unique_ptr into a plain array
|
||||
const char** attributes = nullptr;
|
||||
if (src.nattribute) {
|
||||
attributes = new(std::nothrow) const char*[src.nattribute];
|
||||
if (!attributes) {
|
||||
std::snprintf(err, sizeof(err), "failed to allocate memory for plugin attribute array");
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < src.nattribute; ++i) {
|
||||
attributes[i] = attributes_list[i].release();
|
||||
}
|
||||
}
|
||||
|
||||
dst = src;
|
||||
dst.name = name.release();
|
||||
dst.attributes = attributes;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <>
|
||||
const char* GlobalTable<mjpResourceProvider>::HumanReadableTypeName() {
|
||||
return "resource provider";
|
||||
}
|
||||
|
||||
template <>
|
||||
std::string_view GlobalTable<mjpResourceProvider>::ObjectKey(const mjpResourceProvider& plugin) {
|
||||
return std::string_view(plugin.prefix, strklen(plugin.prefix));
|
||||
}
|
||||
|
||||
// check if two resource providers are identical
|
||||
template <>
|
||||
bool GlobalTable<mjpResourceProvider>::ObjectEqual(const mjpResourceProvider& p1, const mjpResourceProvider& p2) {
|
||||
return (CaseInsensitiveEqual(p1.prefix, p2.prefix) &&
|
||||
p1.open == p2.open &&
|
||||
p1.read == p2.read &&
|
||||
p1.close == p2.close &&
|
||||
p1.getdir == p2.getdir &&
|
||||
p1.modified == p2.modified &&
|
||||
p1.data == p2.data);
|
||||
}
|
||||
|
||||
template <>
|
||||
bool GlobalTable<mjpResourceProvider>::CopyObject(mjpResourceProvider& dst, const mjpResourceProvider& src, ErrorMessage& err) {
|
||||
// copy prefix
|
||||
std::unique_ptr<char[]> prefix = CopyName(src.prefix);
|
||||
if (!prefix) {
|
||||
if (strklen(src.prefix) == -1) {
|
||||
std::snprintf(err, sizeof(err),
|
||||
"provider->prefix length exceeds the maximum limit of %d", kMaxNameLength);
|
||||
} else {
|
||||
std::snprintf(err, sizeof(err), "failed to allocate memory for resource provider prefix");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
dst = src;
|
||||
dst.prefix = prefix.release();
|
||||
return true;
|
||||
}
|
||||
|
||||
// globally register a plugin (thread-safe), return new slot id
|
||||
int mjp_registerPlugin(const mjpPlugin* plugin) {
|
||||
if (!plugin->name) {
|
||||
@@ -314,196 +278,34 @@ int mjp_registerPlugin(const mjpPlugin* plugin) {
|
||||
kMaxAttributes);
|
||||
}
|
||||
|
||||
char err[512];
|
||||
err[0] = '\0';
|
||||
|
||||
// ========= ATTENTION! ==========================================================================
|
||||
// Do not handle objects with nontrivial destructors outside of this lambda.
|
||||
// Do not call mju_error inside this lambda.
|
||||
int slot = [&]() -> int {
|
||||
// check and copy the plugin name
|
||||
std::unique_ptr<char[]> name = CopyName(plugin->name);
|
||||
if (!name) {
|
||||
if (strklen(plugin->name) == -1) {
|
||||
std::snprintf(err, sizeof(err),
|
||||
"plugin->name length exceeds the maximum limit of %d", kMaxNameLength);
|
||||
} else {
|
||||
std::snprintf(err, sizeof(err), "failed to allocate memory for plugin name");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// check and copy plugin attributes
|
||||
std::vector<std::unique_ptr<char[]>> attributes_vec;
|
||||
if (plugin->nattribute) {
|
||||
attributes_vec.reserve(plugin->nattribute);
|
||||
for (int i = 0; i < plugin->nattribute; ++i) {
|
||||
std::unique_ptr<char[]> attr = CopyName(plugin->attributes[i]);
|
||||
if (!attr) {
|
||||
if (strklen(plugin->attributes[i]) == -1) {
|
||||
std::snprintf(
|
||||
err, sizeof(err),
|
||||
"plugin->attributes[%d] exceeds the maximum limit of %d", i, kMaxAttributes);
|
||||
} else {
|
||||
std::snprintf(err, sizeof(err), "failed to allocate memory for plugin attribute");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
attributes_vec.emplace_back(std::move(attr));
|
||||
}
|
||||
}
|
||||
|
||||
Global<mjpPlugin>& global = GetGlobal<mjpPlugin>();
|
||||
auto lock = global.lock_mutex_exclusively();
|
||||
|
||||
int count = global.count().load(std::memory_order_acquire);
|
||||
int local_idx = 0;
|
||||
PluginTable<mjpPlugin>* table = &global.table();
|
||||
|
||||
// check if a non-identical plugin with the same name has already been registered
|
||||
for (int i = 0; i < count; ++i, ++local_idx) {
|
||||
if (local_idx == PluginTable<mjpPlugin>::kBlockSize) {
|
||||
local_idx = 0;
|
||||
table = table->next;
|
||||
}
|
||||
mjpPlugin& existing = table->plugins[local_idx];
|
||||
if (std::strcmp(plugin->name, existing.name) == 0) {
|
||||
if (PluginsAreIdentical(*plugin, existing)) {
|
||||
return i;
|
||||
} else {
|
||||
std::snprintf(err, sizeof(err), "plugin '%s' is already registered", plugin->name);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// allocate a new block of PluginTable if the last allocated block is full
|
||||
if (local_idx == PluginTable<mjpPlugin>::kBlockSize) {
|
||||
local_idx = 0;
|
||||
table = AddNewTableBlock<mjpPlugin>(table);
|
||||
if (!table) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// release the attribute names from unique_ptr into a plain array
|
||||
const char** attributes = nullptr;
|
||||
if (plugin->nattribute) {
|
||||
attributes = new(std::nothrow) const char*[plugin->nattribute];
|
||||
if (!attributes) {
|
||||
std::snprintf(err, sizeof(err), "failed to allocate memory for plugin attribute array");
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < plugin->nattribute; ++i) {
|
||||
attributes[i] = attributes_vec[i].release();
|
||||
}
|
||||
}
|
||||
|
||||
// all checked passed, actually register the plugin into the global table
|
||||
mjpPlugin& registered_plugin = table->plugins[local_idx];
|
||||
registered_plugin = *plugin;
|
||||
registered_plugin.name = name.release();
|
||||
registered_plugin.attributes = attributes;
|
||||
|
||||
// increment the global plugin count with a release memory barrier
|
||||
global.count().store(count + 1, std::memory_order_release);
|
||||
|
||||
return count;
|
||||
}();
|
||||
|
||||
// ========= ATTENTION! ==========================================================================
|
||||
// End of safe lambda, do not handle objects with non-trivial destructors beyond this point.
|
||||
|
||||
// plugin registration failed, throw an mju_error
|
||||
if (slot < 0) {
|
||||
err[sizeof(err) - 1] = '\0';
|
||||
mju_error("%s", err);
|
||||
}
|
||||
|
||||
return slot;
|
||||
return GlobalTable<mjpPlugin>::GetSingleton().AppendIfUnique(*plugin);
|
||||
}
|
||||
|
||||
// look up plugin by slot number, assuming that mjp_pluginCount has already been called
|
||||
const mjpPlugin* mjp_getPluginAtSlotUnsafe(int slot, int nslot) {
|
||||
const mjpPlugin* plugin = GetAtSlot<mjpPlugin>(slot, nslot);
|
||||
if (!plugin || !plugin->name) {
|
||||
return nullptr;
|
||||
}
|
||||
return plugin;
|
||||
return GlobalTable<mjpPlugin>::GetSingleton().GetAtSlotUnsafe(slot, nslot);
|
||||
}
|
||||
|
||||
// look up plugin by name, assuming that mjp_pluginCount has already been called
|
||||
const mjpPlugin* mjp_getPluginUnsafe(const char* name, int* slot, int nslot) {
|
||||
if (slot) *slot = -1;
|
||||
|
||||
if (!name || !name[0]) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Global<mjpPlugin>& plugins = GetGlobal<mjpPlugin>();
|
||||
PluginTable<mjpPlugin>* table = &plugins.table();
|
||||
int found_slot = 0;
|
||||
while (table) {
|
||||
for (int i = 0;
|
||||
i < PluginTable<mjpPlugin>::kBlockSize && found_slot < nslot;
|
||||
++i, ++found_slot) {
|
||||
const mjpPlugin& plugin = table->plugins[i];
|
||||
|
||||
// reached an uninitialized plugin, which means that iterated beyond the plugin count
|
||||
// this should never happen if `count` was actually returned by mjp_pluginCount
|
||||
if (!plugin.name) {
|
||||
return nullptr;
|
||||
}
|
||||
if (std::strcmp(plugin.name, name) == 0) {
|
||||
if (slot) *slot = found_slot;
|
||||
return &plugin;
|
||||
}
|
||||
}
|
||||
table = table->next;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
return GlobalTable<mjpPlugin>::GetSingleton().GetByKeyUnsafe(name, slot, nslot);
|
||||
}
|
||||
|
||||
// return the number of globally registered plugins
|
||||
int mjp_pluginCount() {
|
||||
return GetGlobal<mjpPlugin>().count().load(std::memory_order_acquire);
|
||||
return GlobalTable<mjpPlugin>::GetSingleton().count();
|
||||
}
|
||||
|
||||
// look up a plugin by slot number
|
||||
const mjpPlugin* mjp_getPluginAtSlot(int slot) {
|
||||
const int count = mjp_pluginCount();
|
||||
|
||||
// mjp_pluginCount uses memory_order_acquire which acts as a barrier that guarantees that all
|
||||
// plugins up to `count` have been completely inserted
|
||||
return mjp_getPluginAtSlotUnsafe(slot, count);
|
||||
return GlobalTable<mjpPlugin>::GetSingleton().GetAtSlot(slot);
|
||||
}
|
||||
|
||||
// look up a plugin by name, optionally also get its registered slot number
|
||||
const mjpPlugin* mjp_getPlugin(const char* name, int* slot) {
|
||||
const int count = mjp_pluginCount();
|
||||
int found_slot = -1;
|
||||
const mjpPlugin* plugin = mjp_getPluginUnsafe(name, &found_slot, count);
|
||||
|
||||
if (slot) *slot = found_slot;
|
||||
return plugin;
|
||||
return GlobalTable<mjpPlugin>::GetSingleton().GetByKey(name, slot);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// seek the nth config attrib of a plugin instance by counting null terminators
|
||||
const char* PluginAttrSeek(const mjModel* m, int plugin_id, int attrib_id) {
|
||||
const char* ptr = m->plugin_attr + m->plugin_attradr[plugin_id];
|
||||
for (int i = 0; i < attrib_id; ++i) {
|
||||
while (*ptr) {
|
||||
++ptr;
|
||||
}
|
||||
++ptr;
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
// return a config attribute of a plugin instance
|
||||
// NULL: invalid plugin instance ID or attribute name
|
||||
const char* mj_getPluginConfig(const mjModel* m, int plugin_id, const char* attrib) {
|
||||
@@ -544,95 +346,16 @@ int mjp_registerResourceProvider(const mjpResourceProvider* provider) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char err[512];
|
||||
err[0] = '\0';
|
||||
|
||||
// ========= ATTENTION! ==========================================================================
|
||||
// Do not handle objects with nontrivial destructors outside of this lambda.
|
||||
// Do not call mju_error inside this lambda.
|
||||
int slot = [&]() -> int {
|
||||
std::unique_ptr<char[]> prefix;
|
||||
|
||||
// copy prefix
|
||||
prefix = CopyName(provider->prefix);
|
||||
if (!prefix) {
|
||||
if (strklen(provider->prefix) == -1) {
|
||||
std::snprintf(err, sizeof(err),
|
||||
"provider->prefix length exceeds the maximum limit of %d", kMaxNameLength);
|
||||
} else {
|
||||
std::snprintf(err, sizeof(err), "failed to allocate memory for resource provider prefix");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
Global<mjpResourceProvider>& global = GetGlobal<mjpResourceProvider>();
|
||||
auto lock = global.lock_mutex_exclusively();
|
||||
int count = global.count().load(std::memory_order_acquire);
|
||||
int local_idx = 0;
|
||||
PluginTable<mjpResourceProvider>* table = &global.table();
|
||||
|
||||
// check if a non-identical provider with the same name has already been registered
|
||||
for (int i = 0; i < count; ++i, ++local_idx) {
|
||||
if (local_idx == PluginTable<mjpResourceProvider>::kBlockSize) {
|
||||
local_idx = 0;
|
||||
table = table->next;
|
||||
}
|
||||
mjpResourceProvider& existing = table->plugins[local_idx];
|
||||
|
||||
if (existing.prefix != nullptr) {
|
||||
// if identical then return slot number
|
||||
if (PrefixesAreIdentical(provider->prefix, existing.prefix)) {
|
||||
if (ResourceProvidersAreIdentical(provider, &existing)) {
|
||||
return i;
|
||||
} else {
|
||||
std::snprintf(err, sizeof(err),
|
||||
"a resource provider with prefix '%s' cannot be registered",
|
||||
provider->prefix);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// allocate a new block of PluginTable if the last allocated block is full
|
||||
if (local_idx == PluginTable<mjpResourceProvider>::kBlockSize) {
|
||||
local_idx = 0;
|
||||
table = AddNewTableBlock<mjpResourceProvider>(table);
|
||||
if (!table) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// all checks passed, register the plugin into the global table
|
||||
mjpResourceProvider& registered_provider = table->plugins[local_idx];
|
||||
registered_provider = *provider;
|
||||
registered_provider.prefix = prefix.release();
|
||||
|
||||
// increment the global plugin count with a release memory barrier
|
||||
global.count().store(count + 1, std::memory_order_release);
|
||||
return count;
|
||||
}();
|
||||
|
||||
// ========= ATTENTION! ==========================================================================
|
||||
// End of safe lambda, do not handle objects with non-trivial destructors beyond this point.
|
||||
|
||||
// plugin registration failed, throw a warning
|
||||
if (slot < 0) {
|
||||
err[sizeof(err) - 1] = '\0';
|
||||
mju_warning("%s", err);
|
||||
}
|
||||
|
||||
return slot+1;
|
||||
return GlobalTable<mjpResourceProvider>::GetSingleton().AppendIfUnique(*provider) + 1;
|
||||
}
|
||||
|
||||
// return the number of globally registered resource providers
|
||||
int mjp_resourceProviderCount() {
|
||||
return GetGlobal<mjpResourceProvider>().count().load(std::memory_order_acquire);
|
||||
return GlobalTable<mjpResourceProvider>::GetSingleton().count();
|
||||
}
|
||||
|
||||
// look up a resource provider that matches its prefix against the given resource scheme
|
||||
const mjpResourceProvider* mjp_getResourceProvider(const char* resource_name) {
|
||||
const int count = mjp_resourceProviderCount();
|
||||
if (!resource_name || !resource_name[0]) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -650,41 +373,13 @@ const mjpResourceProvider* mjp_getResourceProvider(const char* resource_name) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Global<mjpResourceProvider>& global = GetGlobal<mjpResourceProvider>();
|
||||
auto lock = global.lock_mutex_exclusively();
|
||||
PluginTable<mjpResourceProvider>* table = &global.table();
|
||||
int found_slot = 0;
|
||||
|
||||
while (table) {
|
||||
for (int i = 0;
|
||||
i < PluginTable<mjpPlugin>::kBlockSize && found_slot < count;
|
||||
++i, ++found_slot) {
|
||||
const mjpResourceProvider& provider = table->plugins[i];
|
||||
const char *prefix = provider.prefix;
|
||||
|
||||
if (prefix != nullptr &&
|
||||
PrefixesAreIdentical(prefix, file_prefix.c_str())) {
|
||||
return &provider;
|
||||
}
|
||||
}
|
||||
table = table->next;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
return GlobalTable<mjpResourceProvider>::GetSingleton().GetByKey(file_prefix.c_str(), nullptr);
|
||||
}
|
||||
|
||||
// look up a resource provider by slot number
|
||||
const mjpResourceProvider* mjp_getResourceProviderAtSlot(int slot) {
|
||||
// mjp_resourceProviderCount uses memory_order_acquire which acts as a barrier
|
||||
// that guarantees that all providers up to `count` have been completely inserted
|
||||
const int count = mjp_resourceProviderCount();
|
||||
|
||||
// shift slot to be zero-indexed
|
||||
const mjpResourceProvider* provider = GetAtSlot<mjpResourceProvider>(slot - 1, count);
|
||||
if (!provider || provider->prefix[0] == '\0') {
|
||||
return nullptr;
|
||||
}
|
||||
return provider;
|
||||
return GlobalTable<mjpResourceProvider>::GetSingleton().GetAtSlot(slot - 1);
|
||||
}
|
||||
|
||||
// load plugins from a dynamic library
|
||||
@@ -704,9 +399,9 @@ void mj_loadAllPluginLibraries(const char* directory,
|
||||
int nplugin_before;
|
||||
int nplugin_after;
|
||||
|
||||
Global<mjpPlugin>& global = GetGlobal<mjpPlugin>();
|
||||
auto& plugin_table = GlobalTable<mjpPlugin>::GetSingleton();
|
||||
{
|
||||
auto lock = global.lock_mutex_exclusively();
|
||||
auto lock = plugin_table.LockExclusively();
|
||||
nplugin_before = mjp_pluginCount();
|
||||
mj_loadPluginLibrary(dso_path.c_str());
|
||||
nplugin_after = mjp_pluginCount();
|
||||
|
||||
Reference in New Issue
Block a user