f5d3ce3451
Add a new mjpEncoder plugin type mirroring the existing mjpDecoder pattern. Encoders serialize an mjSpec + mjModel to an mjResource for a given format. New API functions: - mjp_registerEncoder: globally register an encoder - mjp_defaultEncoder: zero-initialize an encoder struct - mjp_findEncoder: look up an encoder by filename extension or content type The mjfEncode callback takes (mjSpec*, mjModel*, mjVFS*, mjResource*) and returns 0 on success. Writing to mjResource keeps symmetry with the decoder reading from mjResource and leaves the door open for writable resource providers. PiperOrigin-RevId: 889187898 Change-Id: I180771b2255b91dea188ac5e2cdc3a8f0fb85364
780 lines
24 KiB
C++
780 lines
24 KiB
C++
// Copyright 2022 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.
|
|
|
|
// Plugin registration is implemented in C++, unlike the rest of the engine code which is in C.
|
|
// This is because C++ provides a standard cross-platform mutex, which we use to guard the global
|
|
// plugin table in order to make the API thread-safe. We only expose a C API externally, and this
|
|
// entire file can in principle be re-implemented in C if necessary, without breaking any external
|
|
// or internal MuJoCo code elsewhere.
|
|
|
|
#include "engine/engine_plugin.h"
|
|
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <new>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
|
|
extern "C" {
|
|
#if defined(_WIN32) || defined(__CYGWIN__)
|
|
#include <windows.h>
|
|
#else
|
|
#include <dirent.h>
|
|
#include <dlfcn.h>
|
|
#include <sys/stat.h>
|
|
#endif
|
|
}
|
|
|
|
#include <mujoco/mjplugin.h>
|
|
#include "engine/engine_global_table.h"
|
|
#include "engine/engine_util_errmem.h"
|
|
|
|
// set default plugin definition
|
|
void mjp_defaultPlugin(mjpPlugin* plugin) {
|
|
std::memset(plugin, 0, sizeof(*plugin));
|
|
}
|
|
|
|
namespace {
|
|
using mujoco::GlobalTable;
|
|
|
|
constexpr int kMaxNameLength = 1024;
|
|
constexpr int kMaxAttributes = 255;
|
|
|
|
// 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) {
|
|
if (!s[i]) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// return filename extension
|
|
std::string getext(std::string_view filename) {
|
|
size_t dot = filename.find_last_of('.');
|
|
|
|
if (dot == std::string::npos) {
|
|
return "";
|
|
}
|
|
return std::string(filename.substr(dot, filename.size() - dot));
|
|
}
|
|
|
|
// copy a null-terminated string into a new heap-allocated char array managed by a unique_ptr
|
|
std::unique_ptr<char[]> CopyName(const char* s) {
|
|
int len = strklen(s);
|
|
if (len == -1) {
|
|
return nullptr;
|
|
}
|
|
std::unique_ptr<char[]> out(new(std::nothrow) char[len + 1]);
|
|
if (!out) {
|
|
return nullptr;
|
|
}
|
|
std::strncpy(out.get(), s, len);
|
|
out.get()[len] = '\0';
|
|
return out;
|
|
}
|
|
|
|
// check if prefix is a valid URI scheme format
|
|
bool IsValidURISchemeFormat(const char* prefix) {
|
|
int len;
|
|
|
|
// prefix is NULL or empty
|
|
if (prefix == nullptr || !(len = std::strlen(prefix))) {
|
|
return false;
|
|
}
|
|
|
|
// first character must be a letter
|
|
if (!std::isalpha(prefix[0])) {
|
|
return false;
|
|
}
|
|
|
|
for (int i = 1; i < len; i++) {
|
|
// each following character must be a letter, digit, '+', '.', or '-'
|
|
if (!std::isalnum(prefix[i]) &&
|
|
(prefix[i] != '+') &&
|
|
(prefix[i] != '.') &&
|
|
(prefix[i] != '-')) {
|
|
return false;
|
|
}
|
|
}
|
|
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.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;
|
|
}
|
|
|
|
template <>
|
|
const char* GlobalTable<mjpDecoder>::HumanReadableTypeName() {
|
|
return "resource decoder";
|
|
}
|
|
|
|
template <>
|
|
std::string_view GlobalTable<mjpDecoder>::ObjectKey(const mjpDecoder& decoder) {
|
|
// When registering decoders, if the user provides both a content type and an extension we add two
|
|
// entries to the table. One with content_type set and extension unset, and one with the opposite.
|
|
// This means that within a vector, we will only ever have either content_type or extension.
|
|
if (decoder.content_type) {
|
|
if (int len = strklen(decoder.content_type); len != -1) {
|
|
return std::string_view(decoder.content_type, len);
|
|
}
|
|
}
|
|
return std::string_view(decoder.extension, strklen(decoder.extension));
|
|
}
|
|
|
|
// return true if two resource providers are identical
|
|
template <>
|
|
bool GlobalTable<mjpDecoder>::ObjectEqual(const mjpDecoder& d1, const mjpDecoder& d2) {
|
|
// check content_type
|
|
bool content_type_match = false;
|
|
if (d1.content_type && d2.content_type) {
|
|
content_type_match = CaseInsensitiveEqual(d1.content_type, d2.content_type);
|
|
} else {
|
|
content_type_match = (d1.content_type == d2.content_type);
|
|
}
|
|
|
|
// check extension
|
|
bool extension_match = false;
|
|
if (d1.extension && d2.extension) {
|
|
extension_match = CaseInsensitiveEqual(d1.extension, d2.extension);
|
|
} else {
|
|
extension_match = (d1.extension == d2.extension);
|
|
}
|
|
|
|
return content_type_match && extension_match && d1.decode == d2.decode &&
|
|
d1.can_decode == d2.can_decode;
|
|
}
|
|
|
|
template <>
|
|
bool GlobalTable<mjpDecoder>::CopyObject(mjpDecoder& dst, const mjpDecoder& src, ErrorMessage& err) {
|
|
// Just a list of pointers so copy directly.
|
|
dst = src;
|
|
dst.content_type = nullptr;
|
|
dst.extension = nullptr;
|
|
|
|
if (src.content_type) {
|
|
std::unique_ptr<char[]> content_type = CopyName(src.content_type);
|
|
if (!content_type) {
|
|
if (strklen(src.content_type) == -1) {
|
|
std::snprintf(err, sizeof(err),
|
|
"decoder->content_type length exceeds the maximum limit of %d", kMaxNameLength);
|
|
} else {
|
|
std::snprintf(err, sizeof(err), "failed to allocate memory for decoder content_type");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
dst.content_type = content_type.release();
|
|
}
|
|
|
|
if (src.extension) {
|
|
std::unique_ptr<char[]> extension = CopyName(src.extension);
|
|
if (!extension) {
|
|
if (strklen(src.extension) == -1) {
|
|
std::snprintf(err, sizeof(err),
|
|
"decoder->extension length exceeds the maximum limit of %d", kMaxNameLength);
|
|
} else {
|
|
std::snprintf(err, sizeof(err), "failed to allocate memory for decoder extension");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
dst.extension = extension.release();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
template <>
|
|
const char* GlobalTable<mjpEncoder>::HumanReadableTypeName() {
|
|
return "resource encoder";
|
|
}
|
|
|
|
template <>
|
|
std::string_view GlobalTable<mjpEncoder>::ObjectKey(const mjpEncoder& encoder) {
|
|
if (encoder.content_type) {
|
|
if (int len = strklen(encoder.content_type); len != -1) {
|
|
return std::string_view(encoder.content_type, len);
|
|
}
|
|
}
|
|
return std::string_view(encoder.extension, strklen(encoder.extension));
|
|
}
|
|
|
|
template <>
|
|
bool GlobalTable<mjpEncoder>::ObjectEqual(const mjpEncoder& e1,
|
|
const mjpEncoder& e2) {
|
|
bool content_type_match = false;
|
|
if (e1.content_type && e2.content_type) {
|
|
content_type_match = CaseInsensitiveEqual(e1.content_type, e2.content_type);
|
|
} else {
|
|
content_type_match = (e1.content_type == e2.content_type);
|
|
}
|
|
|
|
bool extension_match = false;
|
|
if (e1.extension && e2.extension) {
|
|
extension_match = CaseInsensitiveEqual(e1.extension, e2.extension);
|
|
} else {
|
|
extension_match = (e1.extension == e2.extension);
|
|
}
|
|
|
|
return content_type_match && extension_match
|
|
&& e1.encode == e2.encode && e1.close_resource == e2.close_resource;
|
|
}
|
|
|
|
template <>
|
|
bool GlobalTable<mjpEncoder>::CopyObject(mjpEncoder& dst, const mjpEncoder& src, ErrorMessage& err) {
|
|
dst = src;
|
|
dst.content_type = nullptr;
|
|
dst.extension = nullptr;
|
|
|
|
if (src.content_type) {
|
|
std::unique_ptr<char[]> content_type = CopyName(src.content_type);
|
|
if (!content_type) {
|
|
if (strklen(src.content_type) == -1) {
|
|
std::snprintf(
|
|
err, sizeof(err),
|
|
"encoder->content_type length exceeds the maximum limit of %d",
|
|
kMaxNameLength);
|
|
} else {
|
|
std::snprintf(err, sizeof(err), "failed to allocate memory for encoder content_type");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
dst.content_type = content_type.release();
|
|
}
|
|
|
|
if (src.extension) {
|
|
std::unique_ptr<char[]> extension = CopyName(src.extension);
|
|
if (!extension) {
|
|
if (strklen(src.extension) == -1) {
|
|
std::snprintf(
|
|
err, sizeof(err),
|
|
"encoder->extension length exceeds the maximum limit of %d",
|
|
kMaxNameLength);
|
|
} else {
|
|
std::snprintf(err, sizeof(err), "failed to allocate memory for encoder extension");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
dst.extension = extension.release();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// globally register a plugin (thread-safe), return new slot id
|
|
int mjp_registerPlugin(const mjpPlugin* plugin) {
|
|
if (!plugin->name) {
|
|
mju_error("plugin->name is a null pointer");
|
|
} else if (plugin->name[0] == '\0') {
|
|
mju_error("plugin->name is an empty string");
|
|
} else if (plugin->nattribute < 0) {
|
|
mju_error("plugin->nattribute is negative");
|
|
} else if (plugin->nattribute > kMaxAttributes) {
|
|
mju_error("plugin->nattribute exceeds the maximum limit of %i",
|
|
kMaxAttributes);
|
|
}
|
|
|
|
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) {
|
|
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) {
|
|
return GlobalTable<mjpPlugin>::GetSingleton().GetByKeyUnsafe(name, slot, nslot);
|
|
}
|
|
|
|
// return the number of globally registered plugins
|
|
int mjp_pluginCount() {
|
|
return GlobalTable<mjpPlugin>::GetSingleton().count();
|
|
}
|
|
|
|
// look up a plugin by slot number
|
|
const mjpPlugin* mjp_getPluginAtSlot(int slot) {
|
|
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) {
|
|
return GlobalTable<mjpPlugin>::GetSingleton().GetByKey(name, slot);
|
|
}
|
|
|
|
// 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) {
|
|
if (plugin_id < 0 || plugin_id >= m->nplugin || attrib == nullptr) {
|
|
return nullptr;
|
|
}
|
|
|
|
const mjpPlugin* plugin = mjp_getPluginAtSlot(m->plugin[plugin_id]);
|
|
if (!plugin) {
|
|
return nullptr;
|
|
}
|
|
|
|
for (int i = 0; i < plugin->nattribute; ++i) {
|
|
if (std::strcmp(plugin->attributes[i], attrib) == 0) {
|
|
return PluginAttrSeek(m, plugin_id, i);
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// set default resource provider definition
|
|
void mjp_defaultResourceProvider(mjpResourceProvider* provider) {
|
|
std::memset(provider, 0, sizeof(*provider));
|
|
}
|
|
|
|
// globally register a resource provider (thread-safe), return new slot id
|
|
int mjp_registerResourceProvider(const mjpResourceProvider* provider) {
|
|
// check if prefix is valid URI scheme format
|
|
if (!IsValidURISchemeFormat(provider->prefix)) {
|
|
mju_warning("provider->prefix is '%s' which is not a valid URI scheme format",
|
|
provider->prefix);
|
|
return -1;
|
|
}
|
|
|
|
if (!provider->open || !provider->read || !provider->close) {
|
|
mju_warning("provider must have the open, read, and close callbacks defined");
|
|
return -1;
|
|
}
|
|
|
|
return GlobalTable<mjpResourceProvider>::GetSingleton().AppendIfUnique(*provider) + 1;
|
|
}
|
|
|
|
// return the number of globally registered resource providers
|
|
int mjp_resourceProviderCount() {
|
|
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) {
|
|
if (!resource_name || !resource_name[0]) {
|
|
return nullptr;
|
|
}
|
|
|
|
const char* ch = std::strchr(resource_name, ':');
|
|
if (ch == nullptr) {
|
|
return nullptr;
|
|
}
|
|
|
|
int n = ch - resource_name;
|
|
std::string file_prefix = std::string(resource_name, n);
|
|
|
|
// return NULL if file_prefix doesn't have a valid URI scheme syntax
|
|
if (!IsValidURISchemeFormat(file_prefix.c_str())) {
|
|
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) {
|
|
// shift slot to be zero-indexed
|
|
return GlobalTable<mjpResourceProvider>::GetSingleton().GetAtSlot(slot - 1);
|
|
}
|
|
|
|
// register a resource decoder
|
|
void mjp_registerDecoder(const mjpDecoder* decoder) {
|
|
if (!decoder->decode || !decoder->can_decode) {
|
|
mju_warning("decoder must provide decode and can_decode callbacks.");
|
|
return;
|
|
}
|
|
|
|
if (!decoder->content_type && !decoder->extension) {
|
|
mju_warning("decoder must provide content_type and/or extensions.");
|
|
return;
|
|
}
|
|
|
|
mjpDecoder decoder_copy = *decoder;
|
|
|
|
// Register with content_type
|
|
if (decoder->content_type) {
|
|
decoder_copy.extension = nullptr;
|
|
GlobalTable<mjpDecoder>::GetSingleton().AppendIfUnique(decoder_copy);
|
|
}
|
|
|
|
// Register with extensions
|
|
if (decoder->extension) {
|
|
decoder_copy.content_type = nullptr;
|
|
std::string extensions_str(decoder->extension);
|
|
std::stringstream ss(extensions_str);
|
|
std::string extension;
|
|
while (std::getline(ss, extension, '|')) {
|
|
if (!extension.empty()) {
|
|
decoder_copy.extension = extension.c_str();
|
|
GlobalTable<mjpDecoder>::GetSingleton().AppendIfUnique(decoder_copy);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// set default resource decoder definition
|
|
void mjp_defaultDecoder(mjpDecoder* decoder) {
|
|
std::memset(decoder, 0, sizeof(*decoder));
|
|
}
|
|
|
|
// find a decoder that can process a given resource and content_type
|
|
const mjpDecoder* mjp_findDecoder(const mjResource* resource, const char* content_type) {
|
|
auto extension = getext(resource->name);
|
|
if (strklen(content_type) == -1 && extension.empty()) {
|
|
mju_warning("Must provide extension or content_type to mjp_findDecoder.");
|
|
return nullptr;
|
|
}
|
|
|
|
if (strklen(content_type) > 0) {
|
|
auto* decoder = GlobalTable<mjpDecoder>::GetSingleton().GetByKey(content_type, nullptr);
|
|
if (decoder && decoder->can_decode(resource)) {
|
|
return decoder;
|
|
}
|
|
}
|
|
|
|
if (!extension.empty()) {
|
|
auto* decoder = GlobalTable<mjpDecoder>::GetSingleton().GetByKey(extension.c_str(), nullptr);
|
|
if (decoder && decoder->can_decode(resource)) {
|
|
return decoder;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void mjp_registerEncoder(const mjpEncoder* encoder) {
|
|
if (!encoder->encode) {
|
|
mju_warning("encoder must provide an encode callback.");
|
|
return;
|
|
}
|
|
|
|
if (!encoder->close_resource) {
|
|
mju_warning("encoder must provide a close_resource callback.");
|
|
return;
|
|
}
|
|
|
|
if (!encoder->content_type && !encoder->extension) {
|
|
mju_warning("encoder must provide content_type and/or extensions.");
|
|
return;
|
|
}
|
|
|
|
mjpEncoder encoder_copy = *encoder;
|
|
|
|
if (encoder->content_type) {
|
|
encoder_copy.extension = nullptr;
|
|
GlobalTable<mjpEncoder>::GetSingleton().AppendIfUnique(encoder_copy);
|
|
}
|
|
|
|
if (encoder->extension) {
|
|
encoder_copy.content_type = nullptr;
|
|
std::string extensions_str(encoder->extension);
|
|
std::stringstream ss(extensions_str);
|
|
std::string extension;
|
|
while (std::getline(ss, extension, '|')) {
|
|
if (!extension.empty()) {
|
|
encoder_copy.extension = extension.c_str();
|
|
GlobalTable<mjpEncoder>::GetSingleton().AppendIfUnique(encoder_copy);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void mjp_defaultEncoder(mjpEncoder* encoder) {
|
|
std::memset(encoder, 0, sizeof(*encoder));
|
|
}
|
|
|
|
const mjpEncoder* mjp_findEncoder(const char* filename,
|
|
const char* content_type) {
|
|
auto extension = getext(filename ? filename : "");
|
|
bool has_content_type = content_type && strklen(content_type) > 0;
|
|
if (!has_content_type && extension.empty()) {
|
|
mju_warning("Must provide extension or content_type to mjp_findEncoder.");
|
|
return nullptr;
|
|
}
|
|
|
|
if (has_content_type) {
|
|
auto* encoder =
|
|
GlobalTable<mjpEncoder>::GetSingleton().GetByKey(content_type, nullptr);
|
|
if (encoder) {
|
|
return encoder;
|
|
}
|
|
}
|
|
|
|
if (!extension.empty()) {
|
|
auto* encoder = GlobalTable<mjpEncoder>::GetSingleton().GetByKey(
|
|
extension.c_str(), nullptr);
|
|
if (encoder) {
|
|
return encoder;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// load plugins from a dynamic library
|
|
void mj_loadPluginLibrary(const char* path) {
|
|
#if defined(_WIN32) || defined(__CYGWIN__)
|
|
LoadLibraryA(path);
|
|
#else
|
|
void* handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
|
if (!handle) {
|
|
const char* error = dlerror();
|
|
if (error) {
|
|
mju_error("Error loading plugin library '%s': %s\n", path, error);
|
|
} else {
|
|
mju_error("Unknown error loading plugin library '%s'\n", path);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// scan a directory and load all dynamic libraries
|
|
void mj_loadAllPluginLibraries(const char* directory,
|
|
mjfPluginLibraryLoadCallback callback) {
|
|
auto load_dso_and_call_callback = [&](const std::string& filename,
|
|
const std::string& dso_path) {
|
|
int nplugin_before;
|
|
int nplugin_after;
|
|
|
|
auto& plugin_table = GlobalTable<mjpPlugin>::GetSingleton();
|
|
{
|
|
auto lock = plugin_table.LockExclusively();
|
|
nplugin_before = mjp_pluginCount();
|
|
mj_loadPluginLibrary(dso_path.c_str());
|
|
nplugin_after = mjp_pluginCount();
|
|
}
|
|
|
|
if (callback) {
|
|
int count = nplugin_after - nplugin_before;
|
|
int first = count ? nplugin_before : -1;
|
|
callback(filename.c_str(), first, count);
|
|
}
|
|
};
|
|
|
|
// define platform-specific strings
|
|
#if defined(_WIN32) || defined(__CYGWIN__)
|
|
const std::string sep = "\\";
|
|
WIN32_FIND_DATAA find_data;
|
|
HANDLE hfile = FindFirstFileA(
|
|
(directory + sep + "*.dll").c_str(), &find_data);
|
|
if (!hfile) {
|
|
return;
|
|
}
|
|
|
|
// go through each file in the directory
|
|
bool keep_going = true;
|
|
while (keep_going) {
|
|
const std::string name(find_data.cFileName);
|
|
// load the library and check for plugins
|
|
const std::string dso_path = directory + sep + name;
|
|
load_dso_and_call_callback(name.c_str(), dso_path.c_str());
|
|
keep_going = FindNextFileA(hfile, &find_data);
|
|
}
|
|
FindClose(hfile);
|
|
#else
|
|
const std::string sep = "/";
|
|
#if defined(__APPLE__)
|
|
const std::string dso_suffix = ".dylib";
|
|
#else
|
|
const std::string dso_suffix = ".so";
|
|
#endif
|
|
|
|
DIR* dirp = opendir(directory);
|
|
if (!dirp) {
|
|
return;
|
|
}
|
|
|
|
// go through each entry in the directory
|
|
for (struct dirent* dp; (dp = readdir(dirp));) {
|
|
const std::string name(dp->d_name);
|
|
if (name.size() > dso_suffix.size() &&
|
|
name.substr(name.size() - dso_suffix.size()) == dso_suffix) {
|
|
const std::string dso_path = directory + sep + name;
|
|
|
|
// use stat to resolve symlinks and check that the target is a regular file
|
|
struct stat file_stat;
|
|
if (stat(dso_path.c_str(), &file_stat) == 0 && S_ISREG(file_stat.st_mode)) {
|
|
load_dso_and_call_callback(name.c_str(), dso_path.c_str());
|
|
}
|
|
}
|
|
}
|
|
closedir(dirp);
|
|
#endif
|
|
}
|