diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 3585e961..40d1db35 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -3069,3 +3069,52 @@ mjp_getPluginAtSlot Look up a plugin by the registered slot number that was returned by mjp_registerPlugin. +.. _mjp_defaultResourceProvider: + +mjp_defaultResourceProvider +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mjp_defaultResourceProvider + +Set default resource provider definition. + +.. _mjp_registerResourceProvider: + +mjp_registerResourceProvider +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mjp_registerResourceProvider + +Globally register a resource provider in a thread-safe manner. The provider must have a prefix +that is not a sub-prefix or super-prefix of any current registered providers. This function +returns a slot number > 0 on success. + +.. _mjp_resourceProviderCount: + +mjp_resourceProviderCount +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mjp_resourceProviderCount + +Return the number of globally registered resource providers. + +.. _mjp_getResourceProvider: + +mjp_getResourceProvider +~~~~~~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mjp_getResourceProvider + +Return the resource provider with the prefix that matches against the resource name. +If no match, return NULL. + +.. _mjp_getResourceProviderAtSlot: + +mjp_getResourceProviderAtSlot +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mjp_getResourceProviderAtSlot + +Look up a resource provider by slot number returned by mjp_registerResourceProvider. +If invalid slot number, return NULL. + diff --git a/doc/changelog.rst b/doc/changelog.rst index 04775635..2e5d7294 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -20,6 +20,9 @@ General equivalent body inertias with ellipsoids instead of the default boxes. - Added documentation for :ref:`engine plugins`. - Added struct information to the ``introspect`` module. +- Added a new extension mechanism called "resource provider" . This extensible mechanism allows MuJoCo + to read assets from data sources other than the local OS filesystem or + the :ref:`Virtual file system`. Python bindings ^^^^^^^^^^^^^^^ diff --git a/doc/includes/references.h b/doc/includes/references.h index 66e4139a..1a0d30d9 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -607,13 +607,25 @@ struct mjLROpt_ { // options for mj_setLengthRange() mjtNum tolrange; // convergence tolerance (relative to range) }; typedef struct mjLROpt_ mjLROpt; -struct mjVFS_ { // virtual file system for loading from memory - int nfile; // number of files present +struct mjVFS_ { // virtual file system for loading from memory + int nfile; // number of files present char filename[mjMAXVFS][mjMAXVFSNAME]; // file name without path - int filesize[mjMAXVFS]; // file size in bytes - void* filedata[mjMAXVFS]; // buffer with file data + int filesize[mjMAXVFS]; // file size in bytes + void* filedata[mjMAXVFS]; // buffer with file data }; typedef struct mjVFS_ mjVFS; +struct mjResource_ { + char* name; // name of resource (filename, etc) + void* data; // opaque data pointer + const void* provider_data; // opaque resource provider data + + // reading callback from resource provider + int (*read)(struct mjResource_* resource, const void** buffer); + + // closing callback from resource provider + void (*close)(struct mjResource_* resource); +}; +typedef struct mjResource_ mjResource; struct mjOption_ { // physics options // timing parameters mjtNum timestep; // timestep @@ -1186,6 +1198,14 @@ struct mjModel_ { int* names_map; // internal hash map of names (nnames_map x 1) }; typedef struct mjModel_ mjModel; +struct mjpResourceProvider_ { + const char* prefix; // prefix for match against a resource name + mjfOpenResource open; // opening callback + mjfReadResource read; // reading callback + mjfCloseResource close; // closing callback + void* data; // opaque data pointer (resource invariant) +}; +typedef struct mjpResourceProvider_ mjpResourceProvider; typedef enum mjtPluginCapabilityBit_ { mjPLUGIN_ACTUATOR = 1<<0, mjPLUGIN_SENSOR = 1<<1, @@ -2197,4 +2217,9 @@ int mjp_registerPlugin(const mjpPlugin* plugin); int mjp_pluginCount(); const mjpPlugin* mjp_getPlugin(const char* name, int* slot); const mjpPlugin* mjp_getPluginAtSlot(int slot); +void mjp_defaultResourceProvider(mjpResourceProvider* provider); +int mjp_registerResourceProvider(const mjpResourceProvider* provider); +int mjp_resourceProviderCount(); +const mjpResourceProvider* mjp_getResourceProvider(const char* resource_name); +const mjpResourceProvider* mjp_getResourceProviderAtSlot(int slot); // NOLINTEND diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 6350d953..87113392 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -369,15 +369,31 @@ typedef struct mjLROpt_ mjLROpt; //---------------------------------- mjVFS --------------------------------------------------------- -struct mjVFS_ { // virtual file system for loading from memory - int nfile; // number of files present +struct mjVFS_ { // virtual file system for loading from memory + int nfile; // number of files present char filename[mjMAXVFS][mjMAXVFSNAME]; // file name without path - int filesize[mjMAXVFS]; // file size in bytes - void* filedata[mjMAXVFS]; // buffer with file data + int filesize[mjMAXVFS]; // file size in bytes + void* filedata[mjMAXVFS]; // buffer with file data }; typedef struct mjVFS_ mjVFS; +//---------------------------------- mjResource ---------------------------------------------------- + +struct mjResource_ { + char* name; // name of resource (filename, etc) + void* data; // opaque data pointer + const void* provider_data; // opaque resource provider data + + // reading callback from resource provider + int (*read)(struct mjResource_* resource, const void** buffer); + + // closing callback from resource provider + void (*close)(struct mjResource_* resource); +}; +typedef struct mjResource_ mjResource; + + //---------------------------------- mjOption ------------------------------------------------------ struct mjOption_ { // physics options diff --git a/include/mujoco/mjplugin.h b/include/mujoco/mjplugin.h index 4081c697..6b1ab576 100644 --- a/include/mujoco/mjplugin.h +++ b/include/mujoco/mjplugin.h @@ -20,6 +20,33 @@ #include +//---------------------------------- Resource Provider --------------------------------------------- + +#define mjVFS_PREFIX "vfs://" // prefix for VFS providers + +// callback for opeing a resource, returns zero on failure +typedef int (*mjfOpenResource)(mjResource* resource); + +// callback for reading a resource +// return number of bytes stored in buffer, return -1 if error +typedef int (*mjfReadResource)(mjResource* resource, const void** buffer); + +// callback for closing a resource (responsible for freeing any allocated memory) +typedef void (*mjfCloseResource)(mjResource* resource); + +// struct describing a single resource provider +struct mjpResourceProvider_ { + const char* prefix; // prefix for match against a resource name + mjfOpenResource open; // opening callback + mjfReadResource read; // reading callback + mjfCloseResource close; // closing callback + void* data; // opaque data pointer (resource invariant) +}; +typedef struct mjpResourceProvider_ mjpResourceProvider; + + +//---------------------------------- Plugins ------------------------------------------------------- + typedef enum mjtPluginCapabilityBit_ { mjPLUGIN_ACTUATOR = 1<<0, mjPLUGIN_SENSOR = 1<<1, diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 9b5723fb..b73500d6 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -1192,6 +1192,25 @@ MJAPI const mjpPlugin* mjp_getPlugin(const char* name, int* slot); // Look up a plugin by the registered slot number that was returned by mjp_registerPlugin. MJAPI const mjpPlugin* mjp_getPluginAtSlot(int slot); +// Set default resource provider definition. +MJAPI void mjp_defaultResourceProvider(mjpResourceProvider* provider); + +// Globally register a resource provider in a thread-safe manner. The provider must have a prefix +// that is not a sub-prefix or super-prefix of any current registered providers. This function +// returns a slot number > 0 on success. +MJAPI int mjp_registerResourceProvider(const mjpResourceProvider* provider); + +// Return the number of globally registered resource providers. +MJAPI int mjp_resourceProviderCount(); + +// Return the resource provider with the prefix that matches against the resource name. +// If no match, return NULL. +MJAPI const mjpResourceProvider* mjp_getResourceProvider(const char* resource_name); + +// Look up a resource provider by slot number returned by mjp_registerResourceProvider. +// If invalid slot number, return NULL. +MJAPI const mjpResourceProvider* mjp_getResourceProviderAtSlot(int slot); + #if defined(__cplusplus) } diff --git a/introspect/codegen/generate_structs.py b/introspect/codegen/generate_structs.py index 31ebb050..956b548f 100644 --- a/introspect/codegen/generate_structs.py +++ b/introspect/codegen/generate_structs.py @@ -41,6 +41,10 @@ _ANONYMOUS_KEY_PATTERN = re.compile(r'\d+:\d+(?=\))') _EXCLUDED = ( 'mjpPlugin', 'mjpPlugin_', + 'mjpResourceProvider', + 'mjpResourceProvider_', + 'mjResource', + 'mjResource_', ) diff --git a/introspect/functions.py b/introspect/functions.py index b60d81cb..ee1e1205 100644 --- a/introspect/functions.py +++ b/introspect/functions.py @@ -7374,4 +7374,69 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Look up a plugin by the registered slot number that was returned by mjp_registerPlugin.', # pylint: disable=line-too-long )), + ('mjp_defaultResourceProvider', + FunctionDecl( + name='mjp_defaultResourceProvider', + return_type=ValueType(name='void'), + parameters=( + FunctionParameterDecl( + name='provider', + type=PointerType( + inner_type=ValueType(name='mjpResourceProvider'), + ), + ), + ), + doc='Set default resource provider definition.', + )), + ('mjp_registerResourceProvider', + FunctionDecl( + name='mjp_registerResourceProvider', + return_type=ValueType(name='int'), + parameters=( + FunctionParameterDecl( + name='provider', + type=PointerType( + inner_type=ValueType(name='mjpResourceProvider', is_const=True), # pylint: disable=line-too-long + ), + ), + ), + doc='Globally register a resource provider in a thread-safe manner. The provider must have a prefix that is not a sub-prefix or super-prefix of any current registered providers. This function returns a slot number > 0 on success.', # pylint: disable=line-too-long + )), + ('mjp_resourceProviderCount', + FunctionDecl( + name='mjp_resourceProviderCount', + return_type=ValueType(name='int'), + parameters=(), + doc='Return the number of globally registered resource providers.', + )), + ('mjp_getResourceProvider', + FunctionDecl( + name='mjp_getResourceProvider', + return_type=PointerType( + inner_type=ValueType(name='mjpResourceProvider', is_const=True), + ), + parameters=( + FunctionParameterDecl( + name='resource_name', + type=PointerType( + inner_type=ValueType(name='char', is_const=True), + ), + ), + ), + doc='Return the resource provider with the prefix that matches against the resource name. If no match, return NULL.', # pylint: disable=line-too-long + )), + ('mjp_getResourceProviderAtSlot', + FunctionDecl( + name='mjp_getResourceProviderAtSlot', + return_type=PointerType( + inner_type=ValueType(name='mjpResourceProvider', is_const=True), + ), + parameters=( + FunctionParameterDecl( + name='slot', + type=ValueType(name='int'), + ), + ), + doc='Look up a resource provider by slot number returned by mjp_registerResourceProvider. If invalid slot number, return NULL.', # pylint: disable=line-too-long + )), ]) diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index 612b5a98..031d834e 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -33,8 +33,6 @@ set(MUJOCO_ENGINE_SRCS engine_derivative.h engine_derivative_fd.c engine_derivative_fd.h - engine_file.c - engine_file.h engine_forward.c engine_forward.h engine_inverse.c @@ -50,6 +48,8 @@ set(MUJOCO_ENGINE_SRCS engine_print.h engine_ray.c engine_ray.h + engine_resource.c + engine_resource.h engine_sensor.c engine_sensor.h engine_setconst.c diff --git a/src/engine/engine_file.c b/src/engine/engine_file.c deleted file mode 100644 index a0c52815..00000000 --- a/src/engine/engine_file.c +++ /dev/null @@ -1,80 +0,0 @@ -// 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. - -#include "engine/engine_file.h" - -#include -#include - -#include "engine/engine_util_errmem.h" - -void* mju_fileToMemory(const char* filename, int* filesize) { - // open file - *filesize = 0; - FILE* fp = fopen(filename, "rb"); - if (!fp) { - return NULL; - } - - // find size - if (fseek(fp, 0, SEEK_END) != 0) { - fclose(fp); - mju_warning("Failed to calculate size for '%s'", filename); - return NULL; - } - - // ensure file size fits in int - long long_filesize = ftell(fp); // NOLINT(runtime/int) - if (long_filesize > INT_MAX) { - fclose(fp); - mju_warning("File size over 2GB is not supported. File: '%s'", filename); - return NULL; - } else if (long_filesize < 0) { - fclose(fp); - mju_warning("Failed to calculate size for '%s'", filename); - return NULL; - } - *filesize = long_filesize; - - // go back to start of file - if (fseek(fp, 0, SEEK_SET) != 0) { - fclose(fp); - mju_warning("Read error while reading '%s'", filename); - return NULL; - } - - // allocate and read - void* buffer = mju_malloc(*filesize); - if (!buffer) { - mju_error("mjFileToMemory: could not allocate memory"); - } - size_t bytes_read = fread(buffer, 1, *filesize, fp); - - // check that read data matches file size - if (bytes_read != *filesize) { // SHOULD NOT OCCUR - if (ferror(fp)) { - fclose(fp); - mju_free(buffer); - *filesize = 0; - mju_warning("Read error while reading '%s'", filename); - return NULL; - } else if (feof(fp)) { - *filesize = bytes_read; - } - } - - // close file, return contents - fclose(fp); - return buffer; -} diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index 80e3224f..0c488262 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -25,6 +25,7 @@ #include #include #include "engine/engine_array_safety.h" +#include "engine/engine_resource.h" #include "engine/engine_macro.h" #include "engine/engine_plugin.h" #include "engine/engine_util_blas.h" @@ -620,82 +621,63 @@ void mj_saveModel(const mjModel* m, const char* filename, void* buffer, int buff -// load model from binary MJB file -// if vfs is not NULL, look up file in vfs before reading from disk -mjModel* mj_loadModel(const char* filename, const mjVFS* vfs) { +// load model from binary MJB resource +static mjModel* _mj_loadModel(const char* filename, int default_provider) { int header[4] = {0}; int expected_header[4] = {ID, sizeof(mjtNum), getnint(), getnptr()}; int info[2000]; int ptrbuf = 0; mjModel *m = 0; - FILE* fp = 0; + mjResource* r = NULL; + + if((r = mju_openResource(filename, default_provider)) == NULL) { + return NULL; + } - // find file in VFS if given const void* buffer = NULL; - int buffer_sz = 0; - if (vfs) { - int i = mj_findFileVFS(vfs, filename); - if (i>=0) { - buffer_sz = vfs->filesize[i]; - buffer = vfs->filedata[i]; - } + int buffer_sz = mju_readResource(r, &buffer); + if (buffer_sz <= 0) { + mju_closeResource(r); + return NULL; } - // open file for reading if no buffer - if (!buffer) { - fp = fopen(filename, "rb"); - if (!fp) { - mju_warning("Could not open file '%s'", filename); - return 0; - } + if (buffer_sz < 4*sizeof(int)) { + mju_warning("Model file has an incomplete header"); + mju_closeResource(r); + return NULL; } - // read header - if (fp) { - if (fread(header, 4, sizeof(int), fp) != 4) { - mju_warning("Model file has an incomplete header"); - return 0; - } - } else { - bufread(header, 4*sizeof(int), buffer_sz, buffer, &ptrbuf); - } + bufread(header, 4*sizeof(int), buffer_sz, buffer, &ptrbuf); // check header for (int i=0; i<4; i++) { if (header[i]!=expected_header[i]) { - if (fp) { - fclose(fp); - } - switch (i) { case 0: mju_warning("Model missing header ID"); - return 0; + mju_closeResource(r); + return NULL; case 1: mju_warning("Model and executable have different floating point precision"); - return 0; + mju_closeResource(r); + return NULL; case 2: mju_warning("Model and executable have different number of ints in mjModel"); - return 0; + mju_closeResource(r); + return NULL; default: mju_warning("Model and executable have different number of pointers in mjModel"); - return 0; + mju_closeResource(r); + return NULL; } } } // read mjModel structure: info only - if (fp) { - if (fread(info, sizeof(int), getnint(), fp) != getnint()) { - mju_warning("Model file does not contain enough ints"); - return 0; - } - } else { - bufread(info, sizeof(int)*getnint(), buffer_sz, buffer, &ptrbuf); - } + bufread(info, sizeof(int)*getnint(), buffer_sz, buffer, &ptrbuf); // allocate new mjModel, check sizes m = mj_makeModel(info[0], info[1], info[2], info[3], info[4], info[5], info[6], @@ -707,90 +689,70 @@ mjModel* mj_loadModel(const char* filename, const mjVFS* vfs) { info[42], info[43], info[44], info[45], info[46], info[47], info[48], info[49], info[50], info[51], info[52]); if (!m || m->nbuffer!=info[getnint()-1]) { - if (fp) { - fclose(fp); - } + mju_closeResource(r); mju_warning("Corrupted model, wrong size parameters"); mj_deleteModel(m); - return 0; + return NULL; } // set info fields memcpy(m, info, sizeof(int)*getnint()); // read options and buffer - if (fp) { - if (fread((void*)&m->opt, sizeof(mjOption), 1, fp) != 1) { - mju_warning("Model file does not have a complete mjOption"); - mj_deleteModel(m); - return 0; - } - if (fread((void*)&m->vis, sizeof(mjVisual), 1, fp) != 1) { - mju_warning("Model file does not have a complete mjVisual"); - mj_deleteModel(m); - return 0; - } - if (fread((void*)&m->stat, sizeof(mjStatistic), 1, fp) != 1) { - mju_warning("Model file does not have a complete mjStatistic"); - mj_deleteModel(m); - return 0; - } - { - MJMODEL_POINTERS_PREAMBLE(m) - #define X(type, name, nr, nc) \ - if (fread(m->name, sizeof(type), (m->nr)*(nc), fp) != (m->nr)*(nc)) { \ - mju_warning("Model file does not contain a large enough buffer"); \ - mj_deleteModel(m); \ - return 0; \ - } - MJMODEL_POINTERS - #undef X - } - } else { - bufread((void*)&m->opt, sizeof(mjOption), buffer_sz, buffer, &ptrbuf); - bufread((void*)&m->vis, sizeof(mjVisual), buffer_sz, buffer, &ptrbuf); - bufread((void*)&m->stat, sizeof(mjStatistic), buffer_sz, buffer, &ptrbuf); - { - MJMODEL_POINTERS_PREAMBLE(m) - #define X(type, name, nr, nc) \ - bufread(m->name, sizeof(type)*(m->nr)*(nc), buffer_sz, buffer, &ptrbuf); - MJMODEL_POINTERS - #undef X - } + bufread((void*)&m->opt, sizeof(mjOption), buffer_sz, buffer, &ptrbuf); + bufread((void*)&m->vis, sizeof(mjVisual), buffer_sz, buffer, &ptrbuf); + bufread((void*)&m->stat, sizeof(mjStatistic), buffer_sz, buffer, &ptrbuf); + { + MJMODEL_POINTERS_PREAMBLE(m) + #define X(type, name, nr, nc) \ + bufread(m->name, sizeof(type)*(m->nr)*(nc), buffer_sz, buffer, &ptrbuf); + MJMODEL_POINTERS + #undef X } - // make sure file size is correct - if (fp) { - if (feof(fp)) { - fclose(fp); - mju_warning("Model file is too small"); - mj_deleteModel(m); - return 0; - } - char dummy; - if (fread(&dummy, 1, 1, fp) || !feof(fp)) { - fclose(fp); - mju_warning("Model file is too large"); - mj_deleteModel(m); - return 0; - } + // make sure buffer is the correct size + if (ptrbuf != buffer_sz) { + mju_closeResource(r); + mju_warning("Model file is too large"); + mj_deleteModel(m); + return NULL; } const char* validationError = mj_validateReferences(m); if (validationError) { + mju_closeResource(r); mju_warning("%s", validationError); mj_deleteModel(m); - return 0; + return NULL; } - if (fp) { - fclose(fp); - } + mju_closeResource(r); return m; } +// load model from binary MJB file +// if vfs is not NULL, look up file in vfs before reading from disk +mjModel* mj_loadModel(const char* filename, const mjVFS* vfs) { + if (vfs == NULL) { + return _mj_loadModel(filename, 0); + } + + int index = mj_registerVfsProvider(vfs); + if (index < 1) { + mju_error("mj_loadModel: could not allocate memory"); + return NULL; + } + + mjModel* model = _mj_loadModel(filename, index); + + mjp_unregisterResourceProvider(index); + return model; +} + + + // de-allocate mjModel void mj_deleteModel(mjModel* m) { if (m) { diff --git a/src/engine/engine_plugin.cc b/src/engine/engine_plugin.cc index 189a0eef..3e0995ea 100644 --- a/src/engine/engine_plugin.cc +++ b/src/engine/engine_plugin.cc @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -62,28 +63,33 @@ constexpr int kMaxAttributes = 255; constexpr int kCacheLine = 64; +// vfs prefix +constexpr const char* kVfsPrefix = mjVFS_PREFIX; + // 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 struct alignas(kCacheLine) PluginTable { static constexpr int kBlockSize = 15; PluginTable() { for (int i = 0; i < kBlockSize; ++i) { - mjp_defaultPlugin(&plugins[i]); + std::memset(&plugins[i], 0, sizeof(plugins[i])); } } - mjpPlugin plugins[kBlockSize]; - PluginTable* next = nullptr; + T plugins[kBlockSize]; + PluginTable* next = nullptr; }; static_assert( - sizeof(PluginTable) / kCacheLine == - sizeof(PluginTable::plugins) / kCacheLine + (sizeof(PluginTable::plugins) % kCacheLine > 0), + sizeof(PluginTable) / kCacheLine == + sizeof(PluginTable::plugins) / kCacheLine + + (sizeof(PluginTable::plugins) % kCacheLine > 0), "PluginTable::next doesn't fit in the same cache line as the end of PluginTable::plugins"); using Mutex = std::shared_mutex; @@ -112,12 +118,13 @@ class ReentrantWriteLock { } }; +template class Global { public: Global() { new(mutex_) Mutex; } - PluginTable& table() { + PluginTable& table() { return table_; } std::atomic_int& count() { @@ -132,7 +139,7 @@ class Global { } private: - PluginTable table_; + PluginTable table_; std::atomic_int count_; // A mutex whose destructor is never run. @@ -143,14 +150,60 @@ class Global { alignas(Mutex) unsigned char mutex_[sizeof(Mutex)]; }; -Global& GetGlobal() { - static Global global; +template +Global& GetGlobal() { + static Global global; static_assert(std::is_trivially_destructible_v); return global; } +// allocate new block for the global table +template +PluginTable* AddNewTableBlock(PluginTable* table) { + char err[512]; + err[0] = '\0'; + +#if defined(MAC_OS_X_VERSION_MIN_REQUIRED) && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_14 + // aligned nothrow new is not available until macOS 10.14 + posix_memalign(reinterpret_cast(&table->next), + alignof(PluginTable), sizeof(PluginTable)); + if (table->next) new(table->next) PluginTable; +#else + table->next = new(std::nothrow) PluginTable; +#endif + 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 +const T* GetAtSlot(int slot, int nslot) { + if (slot < 0 || slot >= nslot) { + return nullptr; + } + + Global& global = GetGlobal(); + PluginTable* 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::kBlockSize) { + local_idx -= PluginTable::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 strnlen(const char* s) { +int strklen(const char* s) { for (int i = 0; i < kMaxNameLength; ++i) { if (!s[i]) { return i; @@ -161,7 +214,7 @@ int strnlen(const char* s) { // copy a null-terminated string into a new heap-allocated char array managed by a unique_ptr std::unique_ptr CopyName(const char* s) { - int len = strnlen(s); + int len = strklen(s); if (len == -1) { return nullptr; } @@ -212,6 +265,15 @@ bool PluginsAreIdentical(const mjpPlugin& plugin1, const mjpPlugin& plugin2) { sizeof(mjpPlugin) - (ptr1 - reinterpret_cast(&plugin1)); return !std::memcmp(ptr1, ptr2, remaining_size); } + +// check if two resource providers are identical +bool ResourceProvidersAreIdentical(const mjpResourceProvider* p1, const mjpResourceProvider* p2) { + return (!std::strcmp(p1->prefix, p2->prefix) && + p1->open == p2->open && + p1->read == p2->read && + p1->close == p2->close && + p1->data == p2->data); +} } // namespace // globally register a plugin (thread-safe), return new slot id @@ -237,7 +299,7 @@ int mjp_registerPlugin(const mjpPlugin* plugin) { // check and copy the plugin name std::unique_ptr name = CopyName(plugin->name); if (!name) { - if (strnlen(plugin->name) == -1) { + if (strklen(plugin->name) == -1) { std::snprintf(err, sizeof(err), "plugin->name length exceeds the maximum limit of %d", kMaxNameLength); } else { @@ -253,7 +315,7 @@ int mjp_registerPlugin(const mjpPlugin* plugin) { for (int i = 0; i < plugin->nattribute; ++i) { std::unique_ptr attr = CopyName(plugin->attributes[i]); if (!attr) { - if (strnlen(plugin->attributes[i]) == -1) { + if (strklen(plugin->attributes[i]) == -1) { std::snprintf( err, sizeof(err), "plugin->attributes[%d] exceeds the maximum limit of %d", i, kMaxAttributes); @@ -266,16 +328,16 @@ int mjp_registerPlugin(const mjpPlugin* plugin) { } } - Global& global = GetGlobal(); + Global& global = GetGlobal(); auto lock = global.lock_mutex_exclusively(); int count = global.count().load(std::memory_order_acquire); int local_idx = 0; - PluginTable* table = &global.table(); + PluginTable* 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::kBlockSize) { + if (local_idx == PluginTable::kBlockSize) { local_idx = 0; table = table->next; } @@ -291,21 +353,12 @@ int mjp_registerPlugin(const mjpPlugin* plugin) { } // allocate a new block of PluginTable if the last allocated block is full - if (local_idx == PluginTable::kBlockSize) { + if (local_idx == PluginTable::kBlockSize) { local_idx = 0; -#if defined(MAC_OS_X_VERSION_MIN_REQUIRED) && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_14 - // aligned nothrow new is not available until macOS 10.14 - posix_memalign(reinterpret_cast(&table->next), - alignof(PluginTable), sizeof(PluginTable)); - if (table->next) new(table->next) PluginTable; -#else - table->next = new(std::nothrow) PluginTable; -#endif - if (!table->next) { - std::snprintf(err, sizeof(err), "failed to allocate memory for the global plugin table"); + table = AddNewTableBlock(table); + if (!table) { return -1; } - table = table->next; } // release the attribute names from unique_ptr into a plain array @@ -347,29 +400,11 @@ int mjp_registerPlugin(const mjpPlugin* plugin) { // look up plugin by slot number, assuming that mjp_pluginCount has already been called const mjpPlugin* mjp_getPluginAtSlotUnsafe(int slot, int nslot) { - if (slot < 0 || slot >= nslot) { + const mjpPlugin* plugin = GetAtSlot(slot, nslot); + if (!plugin || !plugin->name) { return nullptr; } - - Global& global = GetGlobal(); - PluginTable* table = &global.table(); - - // iterate over blocks in the global table until the local index is less the block size - int local_idx = slot; - while (local_idx >= PluginTable::kBlockSize) { - local_idx -= PluginTable::kBlockSize; - table = table->next; - if (!table) { - return nullptr; - } - } - - // local_idx is now a valid index into the current block - const mjpPlugin& plugin = table->plugins[local_idx]; - if (!plugin.name) { - return nullptr; - } - return &plugin; + return plugin; } // look up plugin by name, assuming that mjp_pluginCount has already been called @@ -380,12 +415,12 @@ const mjpPlugin* mjp_getPluginUnsafe(const char* name, int* slot, int nslot) { return nullptr; } - Global& plugins = GetGlobal(); - PluginTable* table = &plugins.table(); + Global& plugins = GetGlobal(); + PluginTable* table = &plugins.table(); int found_slot = 0; while (table) { for (int i = 0; - i < PluginTable::kBlockSize && found_slot < nslot; + i < PluginTable::kBlockSize && found_slot < nslot; ++i, ++found_slot) { const mjpPlugin& plugin = table->plugins[i]; @@ -407,10 +442,9 @@ const mjpPlugin* mjp_getPluginUnsafe(const char* name, int* slot, int nslot) { // return the number of globally registered plugins int mjp_pluginCount() { - return GetGlobal().count().load(std::memory_order_acquire); + return GetGlobal().count().load(std::memory_order_acquire); } - // look up a plugin by slot number const mjpPlugin* mjp_getPluginAtSlot(int slot) { const int count = mjp_pluginCount(); @@ -466,6 +500,247 @@ const char* mj_getPluginConfig(const mjModel* m, int plugin_id, const char* attr 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 against reserved prefixes + int n = std::strlen(provider->prefix), + m = std::strlen(kVfsPrefix); + + // one of the prefixes is a subprefix of the other + if (!std::strncmp(kVfsPrefix, provider->prefix, n) || + !std::strncmp(kVfsPrefix, provider->prefix, m)) { + mju_warning("provider->prefix is '%s' which is reserved", provider->prefix); + return -1; + } + + return mjp_registerResourceProviderInternal(provider); +} + +// internal version of mjp_registerResourceProvider without prechecks on reserved prefixes +int mjp_registerResourceProviderInternal(const mjpResourceProvider* provider) { + if (!provider->prefix || provider->prefix[0] == '\0') { + mju_warning("provider->prefix is an empty string"); + return -1; + } + + if (!provider->open || !provider->read || !provider->close) { + mju_warning("provider must have the open, read, and close callbacks defined"); + 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 { + bool vfs_provider = false; + std::unique_ptr prefix; + + // check if this is a VFS provider + if (!std::strcmp(mjVFS_PREFIX, provider->prefix)) { + vfs_provider = true; + } + + // copy prefix + if (!vfs_provider) { + 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& global = GetGlobal(); + auto lock = global.lock_mutex_exclusively(); + int count = global.count().load(std::memory_order_acquire); + int local_idx = 0; + int free_idx = -1, free_local_idx = -1; + PluginTable* table = &global.table(); + PluginTable* free_table = nullptr; + + // 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::kBlockSize) { + local_idx = 0; + table = table->next; + } + mjpResourceProvider& existing = table->plugins[local_idx]; + + // VFS providers can safely go in open slots + if (vfs_provider && existing.prefix == nullptr && free_idx == -1) { + free_table = table; + free_idx = i; + free_local_idx = local_idx; + + // can skip the rest + break; + } + + if (!vfs_provider) { + int n = std::strlen(provider->prefix); + int m = std::strlen(existing.prefix); + + // one of the prefixes is a subprefix of the other + if (!std::strncmp(existing.prefix, provider->prefix, n) || + !std::strncmp(existing.prefix, provider->prefix, m)) { + // if identical then return slot number + 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 (free_local_idx == -1 && local_idx == PluginTable::kBlockSize) { + local_idx = 0; + table = AddNewTableBlock(table); + if (!table) { + return -1; + } + } + + // all checks passed, register the plugin into the global table + mjpResourceProvider& registered_provider = table->plugins[local_idx]; + if (free_local_idx != -1) { + registered_provider = free_table->plugins[free_local_idx]; + } + + registered_provider = *provider; + registered_provider.prefix = (!vfs_provider) ? prefix.release() : kVfsPrefix; + + // increment the global plugin count with a release memory barrier + if (free_idx == -1) { + free_idx = count; + global.count().store(count + 1, std::memory_order_release); + } + return free_idx; + }(); + + // ========= 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; +} + +// globally unregister resource provider (thread-safe) +// only used for VFS resource providers +void mjp_unregisterResourceProvider(int slot) { + // shift slot to zero-index + slot--; + + if (slot < 0) { + return; + } + + // get global table, acquire lock + Global& global = GetGlobal(); + auto lock = global.lock_mutex_exclusively(); + int count = global.count().load(std::memory_order_acquire); + + if (slot >= count) { + return; + } + + PluginTable* 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::kBlockSize) { + local_idx -= PluginTable::kBlockSize; + table = table->next; + if (!table) { + return; + } + } + + // local_idx is now a valid index into the current block + mjpResourceProvider& provider = table->plugins[local_idx]; + + // no-op for anything other than VFS resource providers + if (provider.prefix == kVfsPrefix) { + provider.prefix = nullptr; + } +} + +// return the number of globally registered resource providers +int mjp_resourceProviderCount() { + return GetGlobal().count().load(std::memory_order_acquire); +} + +// look up a resource provider that matches its prefix against the given resource name +const mjpResourceProvider* mjp_getResourceProvider(const char* resource_name) { + const int count = mjp_resourceProviderCount(); + if (!resource_name || !resource_name[0]) { + return nullptr; + } + + // since multiple VFS resource providers can be registered with the same + // prefix, it doesn't make sense to try to match against them + if (!std::strncmp(kVfsPrefix, resource_name, std::strlen(kVfsPrefix))) { + return nullptr; + } + + Global& global = GetGlobal(); + PluginTable* table = &global.table(); + int found_slot = 0; + + while (table) { + for (int i = 0; + i < PluginTable::kBlockSize && found_slot < count; + ++i, ++found_slot) { + + const mjpResourceProvider& provider = table->plugins[i]; + const char *prefix = provider.prefix; + + if (prefix != nullptr && + !std::strncmp(prefix, resource_name, std::strlen(prefix))) { + return &provider; + } + } + table = table->next; + } + + return 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(slot - 1, count); + if (!provider || provider->prefix[0] == '\0') { + return nullptr; + } + return provider; +} + // load plugins from a dynamic library void mj_loadPluginLibrary(const char* path) { #if defined(_WIN32) || defined(__CYGWIN__) @@ -483,7 +758,7 @@ void mj_loadAllPluginLibraries(const char* directory, int nplugin_before; int nplugin_after; - Global& global = GetGlobal(); + Global& global = GetGlobal(); { auto lock = global.lock_mutex_exclusively(); nplugin_before = mjp_pluginCount(); diff --git a/src/engine/engine_plugin.h b/src/engine/engine_plugin.h index d414c6c1..83fbef83 100644 --- a/src/engine/engine_plugin.h +++ b/src/engine/engine_plugin.h @@ -28,15 +28,33 @@ MJAPI void mjp_defaultPlugin(mjpPlugin* plugin); // globally register a plugin (thread-safe), return new slot id MJAPI int mjp_registerPlugin(const mjpPlugin* plugin); +// globally register a resource provider (thread-safe), return new slot id +MJAPI int mjp_registerResourceProvider(const mjpResourceProvider* provider); + +// globally unregister a resource provider (thread-safe) +MJAPI void mjp_unregisterResourceProvider(int slot); + // return the number of globally registered plugins MJAPI int mjp_pluginCount(); +// return the number of globally registered resource providers +MJAPI int mjp_resourceProviderCount(); + // look up a plugin by name, optionally also get its registered slot number MJAPI const mjpPlugin* mjp_getPlugin(const char* name, int* slot); +// set default resource provider definition +MJAPI void mjp_defaultResourceProvider(mjpResourceProvider* provider); + +// look up a resource provider that matches its prefix against the given resource name +MJAPI const mjpResourceProvider* mjp_getResourceProvider(const char* resource_name); + // look up a plugin by slot number MJAPI const mjpPlugin* mjp_getPluginAtSlot(int slot); +// look up a resource provider by slot number +MJAPI const mjpResourceProvider* mjp_getResourceProviderAtSlot(int slot); + // return a config attribute of a plugin instance // NULL: invalid plugin instance ID or attribute name MJAPI const char* mj_getPluginConfig(const mjModel* m, int plugin_id, const char* attrib); @@ -51,10 +69,13 @@ MJAPI void mj_loadAllPluginLibraries(const char* directory, mjfPluginLibraryLoad // MuJoCo-internal functions beyond this point. // "Unsafe" suffix indicates that improper use of these functions may result in data races. // -// The unsafe functions assume that called mjp_pluginCount has already been called, and that it is -// safe to assume that all plugins up to `count` have been completely written into the global table. +// The unsafe functions assume that mjp_pluginCount has already been called, and that all plugins +// up to `count` have been completely written into the global table. // ================================================================================================= +// internal version of mjp_registerResourceProvider without prechecks on reserved prefixes +MJAPI int mjp_registerResourceProviderInternal(const mjpResourceProvider* provider); + // look up a plugin by name, assuming that mjp_pluginCount has already been called const mjpPlugin* mjp_getPluginUnsafe(const char* name, int* slot, int nslot); diff --git a/src/engine/engine_resource.c b/src/engine/engine_resource.c new file mode 100644 index 00000000..e8e915bc --- /dev/null +++ b/src/engine/engine_resource.c @@ -0,0 +1,215 @@ +// 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. + +#include "engine/engine_resource.h" + +#include +#include +#include +#include + +#include "engine/engine_plugin.h" +#include "engine/engine_util_errmem.h" + +// file buffer used internally for the OS filesystem +typedef struct { + void* buffer; + int nbuffer; +} file_buffer; + + +// open the given resource; if the name doesn't have a prefix matching with a +// resource provider, then the default_provider is used +// if default_provider non-positive, then the OS filesystem is used +mjResource* mju_openResource(const char* name, int default_provider) { + mjResource* resource = (mjResource*) mju_malloc(sizeof(mjResource)); + const mjpResourceProvider* provider = NULL; + if (resource == NULL) { + mju_error("mju_openResource: could not allocate memory"); + return NULL; + } + + // copy name + resource->name = mju_malloc(sizeof(char) * (strlen(name) + 1)); + if (resource->name == NULL) { + mju_free(resource); + mju_error("mju_openResource: could not allocate memory"); + return NULL; + } + strcpy(resource->name, name); + + // find provider based off prefix of name + provider = mjp_getResourceProvider(name); + if (provider != NULL) { + resource->read = provider->read; + resource->close = provider->close; + resource->provider_data = provider->data; + if (provider->open(resource)) { + return resource; + } + + mju_warning("mju_openResource: could not open resource '%s", name); + mju_free(resource->name); + mju_free(resource); + return NULL; + } + + // fallback to default provider + if (default_provider > 0) { + provider = mjp_getResourceProviderAtSlot(default_provider); + if (provider == NULL) { + mju_warning("mju_openResource: unknown resource provider"); + mju_free(resource->name); + mju_free(resource); + return NULL; + } + resource->read = provider->read; + resource->close = provider->close; + resource->provider_data = provider->data; + if (provider->open(resource)) { + return resource; + } + + mju_warning("mju_openResource: could not open resource '%s", name); + mju_free(resource->name); + mju_free(resource); + return NULL; + } + + // lastly fallback to OS filesystem + else { + resource->read = NULL; + resource->close = NULL; + resource->provider_data = NULL; + resource->data = mju_malloc(sizeof(file_buffer)); + file_buffer* fb = (file_buffer*) resource->data; + fb->buffer = mju_fileToMemory(name, &(fb->nbuffer)); + if (fb->buffer == NULL) { + mju_warning("mju_openResource: unknown file '%s'", name); + mju_free(fb); + mju_free(resource->name); + mju_free(resource); + return NULL; + } + } + return resource; +} + + + +// close the given resource; no-op if resource is NULL +void mju_closeResource(mjResource* resource) { + if (resource == NULL) { + return; + } + + // use the resource provider to close resource + if (resource->close) { + resource->close(resource); + } + + // if provider is NULL, then OS filesystem is used + else { + file_buffer* fb = (file_buffer*) resource->data; + mju_free(fb->buffer); + mju_free(fb); + } + + // free name and resource + mju_free(resource->name); + mju_free(resource); +} + + + +// set buffer to bytes read from the resource and return number of bytes in buffer; +// return negative value if error +int mju_readResource(mjResource* resource, const void** buffer) { + if (resource == NULL) { + return 0; + } + + if (resource->read) { + return resource->read(resource, buffer); + } + + + // if provider is NULL, then OS filesystem is used + const file_buffer* fb = (file_buffer*) resource->data; + *buffer = fb->buffer; + return fb->nbuffer; +} + + + +// read file into memory buffer (allocated here with mju_malloc) +void* mju_fileToMemory(const char* filename, int* filesize) { + // open file + *filesize = 0; + FILE* fp = fopen(filename, "rb"); + if (!fp) { + return NULL; + } + + // find size + if (fseek(fp, 0, SEEK_END) != 0) { + fclose(fp); + mju_warning("Failed to calculate size for '%s'", filename); + return NULL; + } + + // ensure file size fits in int + long long_filesize = ftell(fp); // NOLINT(runtime/int) + if (long_filesize > INT_MAX) { + fclose(fp); + mju_warning("File size over 2GB is not supported. File: '%s'", filename); + return NULL; + } else if (long_filesize < 0) { + fclose(fp); + mju_warning("Failed to calculate size for '%s'", filename); + return NULL; + } + *filesize = long_filesize; + + // go back to start of file + if (fseek(fp, 0, SEEK_SET) != 0) { + fclose(fp); + mju_warning("Read error while reading '%s'", filename); + return NULL; + } + + // allocate and read + void* buffer = mju_malloc(*filesize); + if (!buffer) { + mju_error("mjFileToMemory: could not allocate memory"); + } + size_t bytes_read = fread(buffer, 1, *filesize, fp); + + // check that read data matches file size + if (bytes_read != *filesize) { // SHOULD NOT OCCUR + if (ferror(fp)) { + fclose(fp); + mju_free(buffer); + *filesize = 0; + mju_warning("Read error while reading '%s'", filename); + return NULL; + } else if (feof(fp)) { + *filesize = bytes_read; + } + } + + // close file, return contents + fclose(fp); + return buffer; +} diff --git a/src/engine/engine_file.h b/src/engine/engine_resource.h similarity index 50% rename from src/engine/engine_file.h rename to src/engine/engine_resource.h index eac16002..0214fed7 100644 --- a/src/engine/engine_file.h +++ b/src/engine/engine_resource.h @@ -12,13 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef MUJOCO_SRC_ENGINE_ENGINE_FILE_H_ -#define MUJOCO_SRC_ENGINE_ENGINE_FILE_H_ +#ifndef MUJOCO_SRC_ENGINE_ENGINE_RESOURCE_H_ +#define MUJOCO_SRC_ENGINE_ENGINE_RESOURCE_H_ + +#include +#include "engine/engine_plugin.h" #ifdef __cplusplus extern "C" { #endif +// open the given resource; if the name doesn't have a prefix matching with a +// resource provider, then the default_provider is used +// if default_provider non-positive, then the OS filesystem is used +MJAPI mjResource* mju_openResource(const char* name, int default_provider); + +// close the given resource; no-op if resource is NULL +MJAPI void mju_closeResource(mjResource* resource); + +// set buffer to bytes read from the resource and return number of bytes in buffer; +// return negative value if error +MJAPI int mju_readResource(mjResource* resource, const void** buffer); + // read file into memory buffer (allocated here with mju_malloc) void* mju_fileToMemory(const char* filename, int* filesize); @@ -26,4 +41,4 @@ void* mju_fileToMemory(const char* filename, int* filesize); } #endif -#endif // MUJOCO_SRC_ENGINE_ENGINE_FILE_H_ +#endif // MUJOCO_SRC_ENGINE_ENGINE_RESOURCE_H_ diff --git a/src/engine/engine_vfs.c b/src/engine/engine_vfs.c index 08053d6c..75b63125 100644 --- a/src/engine/engine_vfs.c +++ b/src/engine/engine_vfs.c @@ -18,7 +18,8 @@ #include #include "engine/engine_array_safety.h" -#include "engine/engine_file.h" +#include "engine/engine_plugin.h" +#include "engine/engine_resource.h" #include "engine/engine_util_errmem.h" #include "engine/engine_util_misc.h" @@ -154,7 +155,6 @@ int mj_findFileVFS(const mjVFS* vfs, const char* filename) { // strip path char newname[mjMAXVFSNAME]; vfs_strippath(newname, filename); - // find specific file for (int i=0; infile; i++) { if (strncmp(newname, vfs->filename[i], mjMAXVFSNAME)==0) { @@ -210,3 +210,56 @@ void mj_deleteVFS(mjVFS* vfs) { memset(vfs, 0, sizeof(mjVFS)); } + + + +// open callback for the VFS resource provider +static int vfs_open_callback(mjResource* resource) { + if (!resource || !resource->provider_data || !resource->name) { + return 0; + } + + const mjVFS* vfs = (const mjVFS*) resource->provider_data; + return mj_findFileVFS(vfs, resource->name) >= 0; +} + + + +// read callback for the VFS resource provider +static int vfs_read_callback(mjResource* resource, const void** buffer) { + if (!resource || !resource->provider_data) { + *buffer = NULL; + return -1; + } + + const mjVFS* vfs = (const mjVFS*) resource->provider_data; + int i = mj_findFileVFS(vfs, resource->name); + if (i < 0) { + *buffer = NULL; + return -1; + } + + *buffer = vfs->filedata[i]; + return vfs->filesize[i]; +} + + + +// close callback for the VFS resource provider +static void vfs_close_callback(mjResource* resource) { +} + + + +// registers a VFS resource provider; returns the index of the provider +int mj_registerVfsProvider(const mjVFS* vfs) { + mjpResourceProvider provider = { + .prefix = mjVFS_PREFIX, + .open = &vfs_open_callback, + .read = &vfs_read_callback, + .close = &vfs_close_callback, + .data = (void*) vfs + }; + + return mjp_registerResourceProviderInternal(&provider); +} diff --git a/src/engine/engine_vfs.h b/src/engine/engine_vfs.h index 893a8421..7fa0cd40 100644 --- a/src/engine/engine_vfs.h +++ b/src/engine/engine_vfs.h @@ -40,6 +40,9 @@ MJAPI int mj_deleteFileVFS(mjVFS* vfs, const char* filename); // delete all files from VFS MJAPI void mj_deleteVFS(mjVFS* vfs); +// registers a VFS resource provider; returns the index of the provider +MJAPI int mj_registerVfsProvider(const mjVFS* vfs); + #ifdef __cplusplus } #endif diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index 00c83f7e..87cb094f 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -30,13 +30,12 @@ #include #include "cc/array_safety.h" #include "engine/engine_crossplatform.h" -#include "engine/engine_file.h" +#include "engine/engine_resource.h" #include "engine/engine_macro.h" #include "engine/engine_util_blas.h" #include "engine/engine_util_errmem.h" #include "engine/engine_util_solve.h" #include "engine/engine_util_spatial.h" -#include "engine/engine_vfs.h" #include "user/user_model.h" #include "user/user_objects.h" #include "user/user_util.h" @@ -180,7 +179,7 @@ template static T* VecToArray(std::vector& vector, bool clear = // compiler -void mjCMesh::Compile(const mjVFS* vfs) { +void mjCMesh::Compile(int default_provider) { // load file if (!file.empty()) { // remove path from file if necessary @@ -191,11 +190,11 @@ void mjCMesh::Compile(const mjVFS* vfs) { // load STL, OBJ or MSH string ext = mjuu_getext(file); if (!strcasecmp(ext.c_str(), ".stl")) { - LoadSTL(vfs); + LoadSTL(default_provider); } else if (!strcasecmp(ext.c_str(), ".obj")) { - LoadOBJ(vfs); + LoadOBJ(default_provider); } else if (!strcasecmp(ext.c_str(), ".msh")) { - LoadMSH(vfs); + LoadMSH(default_provider); } else { throw mjCError(this, "Unknown mesh file type: %s", file.c_str()); } @@ -584,34 +583,36 @@ void mjCMesh::RemoveRepeated() { // load OBJ mesh -void mjCMesh::LoadOBJ(const mjVFS* vfs) { +void mjCMesh::LoadOBJ(int default_provider) { // make filename string filename = mjuu_makefullname( model->modelfiledir, model->meshdir, file); + mjResource* r = nullptr; - tinyobj::ObjReader objReader; - char* buffer = nullptr; - if (vfs) { - int id = mj_findFileVFS(vfs, filename.c_str()); - if (id >= 0) { - buffer = static_cast(vfs->filedata[id]); - int buffer_sz = vfs->filesize[id]; - // TODO(etom): support .mtl files in the VFS case? - objReader.ParseFromString(std::string(buffer, buffer_sz), std::string()); + // try reading from default provider + if((r = mju_openResource(filename.c_str(), default_provider)) == nullptr) { + // try reading from filesystem + if (default_provider || (r = mju_openResource(filename.c_str(), 0)) == nullptr) { + throw mjCError(this, "could not parse OBJ file '%s'", filename.c_str()); } } - // if not found in vfs, read from file - if (!buffer) { - objReader.ParseFromFile(filename); + tinyobj::ObjReader objReader; + const void* bytes = nullptr; + int buffer_sz = mju_readResource(r, &bytes); + if (buffer_sz < 0) { + mju_closeResource(r); + throw mjCError(this, "could not parse OBJ file '%s'", filename.c_str()); } + // TODO(etom): support .mtl files? + const char* buffer = (const char*) bytes; + objReader.ParseFromString(std::string(buffer, buffer_sz), std::string()); + mju_closeResource(r); + if (!objReader.Valid()) { - std::stringstream msg; - msg << "could not parse OBJ file '" << filename << "': \n" - << objReader.Error(); - throw mjCError(this, "%s", msg.str().c_str()); + throw mjCError(this, "could not parse OBJ file '%s'", filename.c_str()); } const auto& attrib = objReader.GetAttrib(); @@ -685,56 +686,42 @@ void mjCMesh::LoadOBJ(const mjVFS* vfs) { // load STL binary mesh -void mjCMesh::LoadSTL(const mjVFS* vfs) { +void mjCMesh::LoadSTL(int default_provider) { bool righthand = (scale[0]*scale[1]*scale[2]>0); // make filename string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file); + mjResource* r = nullptr; + if((r = mju_openResource(filename.c_str(), default_provider)) == nullptr) { + if(!default_provider || (r = mju_openResource(filename.c_str(), 0)) == nullptr) { + throw mjCError(this, "could not open STL file '%s'", filename.c_str()); + } + } + // get file data in buffer char* buffer = 0; - int buffer_sz = 0; - bool own_buffer = false; - if (vfs) { - int id = mj_findFileVFS(vfs, filename.c_str()); - if (id>=0) { - buffer = (char*)vfs->filedata[id]; - buffer_sz = vfs->filesize[id]; - } - } - - // if not found in vfs, read from file - if (!buffer) { - buffer = (char*) mju_fileToMemory(filename.c_str(), &buffer_sz); - own_buffer = true; - } + int buffer_sz = mju_readResource(r, (const void**) &buffer); // still not found - if (!buffer) { + if (buffer_sz < 0) { + mju_closeResource(r); throw mjCError(this, "could not open STL file '%s'", filename.c_str()); } else if (!buffer_sz) { - if (own_buffer) { - mju_free(buffer); - } + mju_closeResource(r); throw mjCError(this, "STL file '%s' is empty", filename.c_str()); } // make sure there is enough data for header if (buffer_sz<84) { - if (own_buffer) { - mju_free(buffer); - } - + mju_closeResource(r); throw mjCError(this, "invalid header in STL file '%s'", filename.c_str()); } // get number of triangles, check bounds nface = *(unsigned int*)(buffer+80); if (nface<1 || nface>200000) { - if (own_buffer) { - mju_free(buffer); - } - + mju_closeResource(r); throw mjCError(this, "number of faces should be between 1 and 200000 in STL file '%s';" " perhaps this is an ASCII file?", filename.c_str()); @@ -742,10 +729,7 @@ void mjCMesh::LoadSTL(const mjVFS* vfs) { // check remaining buffer size if (nface*50 != buffer_sz-84) { - if (own_buffer) { - mju_free(buffer); - } - + mju_closeResource(r); throw mjCError(this, "STL file '%s' has wrong size; perhaps this is an ASCII file?", filename.c_str()); @@ -765,19 +749,13 @@ void mjCMesh::LoadSTL(const mjVFS* vfs) { float* v = (float*)(stl+50*i+12*(j+1)); for (int k=0; k < 3; k++) { if (std::isnan(v[k]) || std::isinf(v[k])) { - if (own_buffer) { - mju_free(buffer); - } - + mju_closeResource(r); throw mjCError(this, "STL file '%s' contains invalid vertices.", filename.c_str()); } // check if vertex coordinates can be cast to an int safely if (fabs(v[k])>pow(2, 30)) { - if (own_buffer) { - mju_free(buffer); - } - + mju_closeResource(r); throw mjCError(this, "vertex coordinates in STL file '%s' exceed maximum bounds", filename.c_str()); @@ -797,56 +775,43 @@ void mjCMesh::LoadSTL(const mjVFS* vfs) { } } - // free buffer if allocated here - if (own_buffer) { - mju_free(buffer); - } - + mju_closeResource(r); RemoveRepeated(); } // load MSH binary mesh -void mjCMesh::LoadMSH(const mjVFS* vfs) { +void mjCMesh::LoadMSH(int default_provider) { bool righthand = (scale[0]*scale[1]*scale[2]>0); // make filename string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file); + mjResource* r = nullptr; + if((r = mju_openResource(filename.c_str(), default_provider)) == nullptr) { + // fall back to OS filesystem + if(!default_provider || (r = mju_openResource(filename.c_str(), 0)) == nullptr) { + throw mjCError(this, "could not open STL file '%s'", filename.c_str()); + } + } + // get file data in buffer char* buffer = 0; - int buffer_sz = 0; - bool own_buffer = false; - if (vfs) { - int id = mj_findFileVFS(vfs, filename.c_str()); - if (id>=0) { - buffer = (char*)vfs->filedata[id]; - buffer_sz = vfs->filesize[id]; - } - } - - // if not found in vfs, read from file - if (!buffer) { - buffer = (char*) mju_fileToMemory(filename.c_str(), &buffer_sz); - own_buffer = true; - } + int buffer_sz = mju_readResource(r, (const void**) &buffer); // still not found - if (!buffer) { - throw mjCError(this, "could not open MSH file '%s'", filename.c_str()); + if (buffer_sz < 0) { + mju_closeResource(r); + throw mjCError(this, "could not open STL file '%s'", filename.c_str()); } else if (!buffer_sz) { - if (own_buffer) { - mju_free(buffer); - } - throw mjCError(this, "MSH file '%s' is empty", filename.c_str()); + mju_closeResource(r); + throw mjCError(this, "STL file '%s' is empty", filename.c_str()); } // make sure header is present if (buffer_sz<4*sizeof(int)) { - if (own_buffer) { - mju_free(buffer); - } + mju_closeResource(r); throw mjCError(this, "missing header in MSH file '%s'", filename.c_str()); } @@ -860,18 +825,14 @@ void mjCMesh::LoadMSH(const mjVFS* vfs) { if (nvert<4 || nface<0 || nnormal<0 || ntexcoord<0 || (nnormal>0 && nnormal!=nvert) || (ntexcoord>0 && ntexcoord!=nvert)) { - if (own_buffer) { - mju_free(buffer); - } + mju_closeResource(r); throw mjCError(this, "invalid sizes in MSH file '%s'", filename.c_str()); } // check file size if (buffer_sz != 4*sizeof(int) + 3*nvert*sizeof(float) + 3*nnormal*sizeof(float) + 2*ntexcoord*sizeof(float) + 3*nface*sizeof(int)) { - if (own_buffer) { - mju_free(buffer); - } + mju_closeResource(r); throw mjCError(this, "unexpected file size in MSH file '%s'", filename.c_str()); } @@ -912,10 +873,7 @@ void mjCMesh::LoadMSH(const mjVFS* vfs) { } } - // free buffer if allocated here - if (own_buffer) { - mju_free(buffer); - } + mju_closeResource(r); } @@ -1602,7 +1560,7 @@ mjCSkin::~mjCSkin() { // compiler -void mjCSkin::Compile(const mjVFS* vfs) { +void mjCSkin::Compile(int default_provider) { // load file if (!file.empty()) { @@ -1627,7 +1585,7 @@ void mjCSkin::Compile(const mjVFS* vfs) { // load SKN string ext = mjuu_getext(file); if (!strcasecmp(ext.c_str(), ".skn")) { - LoadSKN(vfs); + LoadSKN(default_provider); } else { throw mjCError(this, "Unknown skin file type: %s", file.c_str()); } @@ -1749,43 +1707,32 @@ void mjCSkin::Compile(const mjVFS* vfs) { // load skin in SKN BIN format -void mjCSkin::LoadSKN(const mjVFS* vfs) { +void mjCSkin::LoadSKN(int default_provider) { // make filename string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file); // get file data in buffer - char* buffer = NULL; - int buffer_sz = 0; - bool own_buffer = false; - if (vfs) { - int id = mj_findFileVFS(vfs, filename.c_str()); - if (id>=0) { - buffer = (char*)vfs->filedata[id]; - buffer_sz = vfs->filesize[id]; - } + mjResource* r = nullptr; + if((r = mju_openResource(filename.c_str(), default_provider)) == nullptr) { + if(!default_provider || (r = mju_openResource(filename.c_str(), 0)) == nullptr) { + throw mjCError(this, "could not open SKN file '%s'", filename.c_str()); + } } - // if not found in vfs, read from file - if (!buffer) { - buffer = (char*) mju_fileToMemory(filename.c_str(), &buffer_sz); - own_buffer = true; - } + char* buffer = 0; + int buffer_sz = mju_readResource(r, (const void**) &buffer); - // still not found - if (!buffer) { + if (buffer_sz < 0) { + mju_closeResource(r); throw mjCError(this, "could not open SKN file '%s'", filename.c_str()); } else if (!buffer_sz) { - if (own_buffer) { - mju_free(buffer); - } + mju_closeResource(r); throw mjCError(this, "SKN file '%s' is empty", filename.c_str()); } // make sure header is present if (buffer_sz<16) { - if (own_buffer) { - mju_free(buffer); - } + mju_closeResource(r); throw mjCError(this, "missing header in SKN file '%s'", filename.c_str()); } @@ -1797,17 +1744,13 @@ void mjCSkin::LoadSKN(const mjVFS* vfs) { // negative sizes not allowed if (nvert<0 || ntexcoord<0 || nface<0 || nbone<0) { - if (own_buffer) { - mju_free(buffer); - } + mju_closeResource(r); throw mjCError(this, "negative size in header of SKN file '%s'", filename.c_str()); } // make sure we have data for vert, texcoord, face if (buffer_sz < 16 + 12*nvert + 8*ntexcoord + 12*nface) { - if (own_buffer) { - mju_free(buffer); - } + mju_closeResource(r); throw mjCError(this, "insufficient data in SKN file '%s'", filename.c_str()); } @@ -1847,9 +1790,7 @@ void mjCSkin::LoadSKN(const mjVFS* vfs) { for (int i=0; i(&m), *const_cast(&data), vfs); + TryCompile(*const_cast(&m), *const_cast(&data), default_provider); } catch (mjCError err) { // deallocate everything allocated in Compile mj_deleteModel(m); @@ -2449,7 +2450,7 @@ mjModel* mjCModel::Compile(const mjVFS* vfs) { } -void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { +void mjCModel::TryCompile(mjModel*& m, mjData*& d, int default_provider) { // check if nan test works double test = mjNAN; if (mjuu_defined(test)) { @@ -2526,7 +2527,7 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { // compile meshes (needed for geom compilation) for (int i=0; iCompile(vfs); + meshes[i]->Compile(default_provider); } // automatically set nuser fields @@ -2585,9 +2586,9 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { } // compile all other objects except for keyframes - for (int i=0; iCompile(vfs); - for (int i=0; iCompile(vfs); - for (int i=0; iCompile(vfs); + for (int i=0; iCompile(default_provider); + for (int i=0; iCompile(default_provider); + for (int i=0; iCompile(default_provider); for (int i=0; iCompile(); for (int i=0; iCompile(); for (int i=0; iCompile(); diff --git a/src/user/user_model.h b/src/user/user_model.h index bad9292c..41cc614c 100644 --- a/src/user/user_model.h +++ b/src/user/user_model.h @@ -62,32 +62,32 @@ class mjCModel { friend class mjXWriter; public: - mjCModel(); // constructor - ~mjCModel(); // destructor + mjCModel(); // constructor + ~mjCModel(); // destructor - mjModel* Compile(const mjVFS* vfs = 0); // COMPILER: construct mjModel - bool CopyBack(const mjModel*); // DECOMPILER: copy numeric back - void FuseStatic(void); // fuse static bodies with parent - void FuseReindex(mjCBody* body); // reindex elements during fuse + mjModel* Compile(int default_provider = 0); // COMPILER: construct mjModel + bool CopyBack(const mjModel*); // DECOMPILER: copy numeric back + void FuseStatic(void); // fuse static bodies with parent + void FuseReindex(mjCBody* body); // reindex elements during fuse //------------------------ API for adding model elements - mjCMesh* AddMesh(mjCDef* def = 0); // mesh - mjCSkin* AddSkin(void); // skin - mjCHField* AddHField(void); // heightfield - mjCTexture* AddTexture(void); // texture - mjCMaterial*AddMaterial(mjCDef* def = 0); // material - mjCPair* AddPair(mjCDef* def = 0); // geom pair for inclusion - mjCBodyPair*AddExclude(void); // body pair for exclusion - mjCEquality*AddEquality(mjCDef* def = 0); // equality constraint - mjCTendon* AddTendon(mjCDef* def = 0); // tendon - mjCActuator*AddActuator(mjCDef* def = 0); // actuator - mjCSensor* AddSensor(void); // sensor - mjCNumeric* AddNumeric(void); // custom numeric - mjCText* AddText(void); // custom text - mjCTuple* AddTuple(void); // custom tuple - mjCKey* AddKey(void); // keyframe - mjCPlugin* AddPlugin(void); // plugin instance + mjCMesh* AddMesh(mjCDef* def = 0); // mesh + mjCSkin* AddSkin(void); // skin + mjCHField* AddHField(void); // heightfield + mjCTexture* AddTexture(void); // texture + mjCMaterial* AddMaterial(mjCDef* def = 0); // material + mjCPair* AddPair(mjCDef* def = 0); // geom pair for inclusion + mjCBodyPair* AddExclude(void); // body pair for exclusion + mjCEquality* AddEquality(mjCDef* def = 0); // equality constraint + mjCTendon* AddTendon(mjCDef* def = 0); // tendon + mjCActuator* AddActuator(mjCDef* def = 0); // actuator + mjCSensor* AddSensor(void); // sensor + mjCNumeric* AddNumeric(void); // custom numeric + mjCText* AddText(void); // custom text + mjCTuple* AddTuple(void); // custom tuple + mjCKey* AddKey(void); // keyframe + mjCPlugin* AddPlugin(void); // plugin instance //------------------------ API for access to model elements (outside tree) int NumObjects(mjtObj type); // number of objects in specified list @@ -163,7 +163,8 @@ class mjCModel { int nuser_sensor; // number of mjtNums in sensor_user private: - void TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs); + void TryCompile(mjModel*& m, mjData*& d, int default_provider); + mjModel* _Compile(int default_provider); void Clear(void); // clear objects allocated by Compile diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 2cd6f4ff..d1bd7602 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -29,7 +29,7 @@ #include "cc/array_safety.h" #include "engine/engine_core_smooth.h" #include "engine/engine_crossplatform.h" -#include "engine/engine_file.h" +#include "engine/engine_resource.h" #include "engine/engine_io.h" #include "engine/engine_macro.h" #include "engine/engine_passive.h" @@ -38,7 +38,6 @@ #include "engine/engine_util_misc.h" #include "engine/engine_util_solve.h" #include "engine/engine_util_spatial.h" -#include "engine/engine_vfs.h" #include "user/user_model.h" #include "user/user_util.h" @@ -1979,33 +1978,27 @@ mjCHField::~mjCHField() { // load elevation data from custom format -void mjCHField::LoadCustom(string filename, const mjVFS* vfs) { +void mjCHField::LoadCustom(string filename, int default_provider) { // get file data in buffer - void* buffer = 0; - int buffer_sz = 0, flag_existing = 0; - if (vfs) { - int id = mj_findFileVFS(vfs, filename.c_str()); - if (id>=0) { - buffer = vfs->filedata[id]; - buffer_sz = vfs->filesize[id]; - flag_existing = 1; + const void* buffer = 0; + mjResource* resource = nullptr; + + if((resource = mju_openResource(filename.c_str(), default_provider)) == nullptr) { + // default to OS filesystem + if (!default_provider || (resource = mju_openResource(filename.c_str(), 0)) == nullptr) { + throw mjCError(this, "could not open hfield file '%s'", filename.c_str()); } } - // if not found in vfs, read from file - if (!buffer) { - buffer = mju_fileToMemory(filename.c_str(), &buffer_sz); - } + int buffer_sz = mju_readResource(resource, &buffer); // still not found - if (!buffer || !buffer_sz) { + if (!buffer || buffer_sz < 1) { throw mjCError(this, "could not open hfield file '%s'", filename.c_str()); } if (buffer_sz < 2*sizeof(int)) { - if (!flag_existing) { - mju_free(buffer); - } + mju_closeResource(resource); throw mjCError(this, "hfield missing header '%s'", filename.c_str()); } @@ -2016,76 +2009,69 @@ void mjCHField::LoadCustom(string filename, const mjVFS* vfs) { // check dimensions if (nrow<1 || ncol<1) { - if (!flag_existing) { - mju_free(buffer); - } - + mju_closeResource(resource); throw mjCError(this, "non-positive hfield dimensions in file '%s'", filename.c_str()); } // check buffer size if (buffer_sz != nrow*ncol*sizeof(float)+8) { - if (!flag_existing) { - mju_free(buffer); - } - + mju_closeResource(resource); throw mjCError(this, "unexpected file size in file '%s'", filename.c_str()); } // allocate data = (float*) mju_malloc(nrow*ncol*sizeof(float)); if (!data) { - if (!flag_existing) { - mju_free(buffer); - } - + mju_closeResource(resource); throw mjCError(this, "could not allocate buffers in hfield"); } // copy data memcpy(data, (void*)(pint+2), nrow*ncol*sizeof(float)); - // free buffer if allocated here - if (!flag_existing) { - mju_free(buffer); - } + // close file + mju_closeResource(resource); } // load elevation data from PNG format -void mjCHField::LoadPNG(string filename, const mjVFS* vfs) { +void mjCHField::LoadPNG(string filename, int default_provider) { // determine data source - const unsigned char* inbuffer = 0; - size_t inbuffer_sz = 0; - if (vfs) { - int id = mj_findFileVFS(vfs, filename.c_str()); - if (id>=0) { - inbuffer = (const unsigned char*)vfs->filedata[id]; - inbuffer_sz = (size_t)vfs->filesize[id]; - } + const void* inbuffer = 0; + mjResource* resource = nullptr; + + if((resource = mju_openResource(filename.c_str(), default_provider)) == nullptr) { + throw mjCError(this, "could not open PNG file '%s'", filename.c_str()); + } + + int inbuffer_sz = mju_readResource(resource, &inbuffer); + + // still not found + if (!inbuffer || inbuffer_sz < 1) { + mju_closeResource(resource); + throw mjCError(this, "could not open PNG file '%s'", filename.c_str()); } // load PNG from file or memory unsigned int w, h, err; std::vector image; - if (inbuffer_sz) { - err = lodepng::decode(image, w, h, inbuffer, inbuffer_sz, LCT_GREY, 8); - } else { - err = lodepng::decode(image, w, h, filename, LCT_GREY, 8); - } + err = lodepng::decode(image, w, h, (const unsigned char*) inbuffer, inbuffer_sz, LCT_GREY, 8); // check if (err) { + mju_closeResource(resource); throw mjCError(this, "PNG load error '%s' in hfield id = %d", lodepng_error_text(err), id); } if (!w || !h) { + mju_closeResource(resource); throw mjCError(this, "Zero dimension in PNG hfield '%s' (id = %d)", name.c_str(), id); } // allocate data = (float*) mju_malloc(w*h*sizeof(float)); if (!data) { + mju_closeResource(resource); throw mjCError(this, "could not allocate buffers in hfield"); } @@ -2097,12 +2083,13 @@ void mjCHField::LoadPNG(string filename, const mjVFS* vfs) { data[c+(nrow-1-r)*ncol] = (float)image[c+r*ncol]; } image.clear(); + mju_closeResource(resource); } // compiler -void mjCHField::Compile(const mjVFS* vfs) { +void mjCHField::Compile(int default_provider) { // check size parameters for (int i=0; i<4; i++) if (size[i]<=0) @@ -2128,9 +2115,9 @@ void mjCHField::Compile(const mjVFS* vfs) { // load depending on format string ext = mjuu_getext(filename); if (!strcasecmp(ext.c_str(), ".png")) { - LoadPNG(filename, vfs); + LoadPNG(filename, default_provider); } else { - LoadCustom(filename, vfs); + LoadCustom(filename, default_provider); } } @@ -2441,27 +2428,27 @@ void mjCTexture::BuiltinCube(void) { // load PNG file -void mjCTexture::LoadPNG(string filename, const mjVFS* vfs, +void mjCTexture::LoadPNG(string filename, int default_provider, std::vector& image, unsigned int& w, unsigned int& h) { - // determine data source - const unsigned char* inbuffer = 0; - size_t inbuffer_sz = 0; - if (vfs) { - int id = mj_findFileVFS(vfs, filename.c_str()); - if (id>=0) { - inbuffer = (const unsigned char*)vfs->filedata[id]; - inbuffer_sz = (size_t)vfs->filesize[id]; - } + const void* inbuffer = 0; + mjResource* resource = nullptr; + + if((resource = mju_openResource(filename.c_str(), default_provider)) == nullptr) { + throw mjCError(this, "could not open PNG file '%s'", filename.c_str()); + } + + int inbuffer_sz = mju_readResource(resource, &inbuffer); + + // still not found + if (!inbuffer || inbuffer_sz < 1) { + mju_closeResource(resource); + throw mjCError(this, "could not open PNG file '%s'", filename.c_str()); } // load PNG from file or memory - unsigned int err; - if (inbuffer_sz) { - err = lodepng::decode(image, w, h, inbuffer, inbuffer_sz, LCT_RGB, 8); - } else { - err = lodepng::decode(image, w, h, filename, LCT_RGB, 8); - } + unsigned int err = lodepng::decode(image, w, h, (const unsigned char*) inbuffer, inbuffer_sz, LCT_RGB, 8); + mju_closeResource(resource); // check if (err) { @@ -2476,28 +2463,24 @@ void mjCTexture::LoadPNG(string filename, const mjVFS* vfs, // load custom file -void mjCTexture::LoadCustom(string filename, const mjVFS* vfs, +void mjCTexture::LoadCustom(string filename, int default_provider, std::vector& image, unsigned int& w, unsigned int& h) { - // get file data in buffer - void* buffer = 0; - int buffer_sz = 0, flag_existing = 0; - if (vfs) { - int id = mj_findFileVFS(vfs, filename.c_str()); - if (id>=0) { - buffer = vfs->filedata[id]; - buffer_sz = vfs->filesize[id]; - flag_existing = 1; + const void* buffer = 0; + mjResource* resource = nullptr; + + if((resource = mju_openResource(filename.c_str(), default_provider)) == nullptr) { + // default to OS filesystem + if (!default_provider || (resource = mju_openResource(filename.c_str(), 0)) == nullptr) { + throw mjCError(this, "could not open texture file '%s'", filename.c_str()); } } - // if not found in vfs, read from file - if (!buffer) { - buffer = mju_fileToMemory(filename.c_str(), &buffer_sz); - } + int buffer_sz = mju_readResource(resource, &buffer); // still not found - if (!buffer || !buffer_sz) { + if (!buffer || buffer_sz < 0) { + mju_closeResource(resource); throw mjCError(this, "could not open texture file '%s'", filename.c_str()); } @@ -2508,20 +2491,14 @@ void mjCTexture::LoadCustom(string filename, const mjVFS* vfs, // check dimensions if (w<1 || h<1) { - if (!flag_existing) { - mju_free(buffer); - } - + mju_closeResource(resource); throw mjCError(this, "Non-PNG texture, assuming custom binary file format,\n" "non-positive texture dimensions in file '%s'", filename.c_str()); } // check buffer size if (buffer_sz != 2*sizeof(int) + w*h*3*sizeof(char)) { - if (!flag_existing) { - mju_free(buffer); - } - + mju_closeResource(resource); throw mjCError(this, "Non-PNG texture, assuming custom binary file format,\n" "unexpected file size in file '%s'", filename.c_str()); } @@ -2530,24 +2507,21 @@ void mjCTexture::LoadCustom(string filename, const mjVFS* vfs, image.resize(w*h*3); memcpy(image.data(), (void*)(pint+2), w*h*3*sizeof(char)); - // free buffer if allocated here - if (!flag_existing) { - mju_free(buffer); - } + mju_closeResource(resource); } // load from PNG or custom file, flip if specified -void mjCTexture::LoadFlip(string filename, const mjVFS* vfs, +void mjCTexture::LoadFlip(string filename, int default_provider, std::vector& image, unsigned int& w, unsigned int& h) { // dispatch to PNG or Custom loaded string ext = mjuu_getext(filename); if (!strcasecmp(ext.c_str(), ".png")) { - LoadPNG(filename, vfs, image, w, h); + LoadPNG(filename, default_provider, image, w, h); } else { - LoadCustom(filename, vfs, image, w, h); + LoadCustom(filename, default_provider, image, w, h); } // horizontal flip @@ -2598,11 +2572,11 @@ void mjCTexture::LoadFlip(string filename, const mjVFS* vfs, // load 2D -void mjCTexture::Load2D(string filename, const mjVFS* vfs) { +void mjCTexture::Load2D(string filename, int default_provider) { // load PNG or custom unsigned int w, h; std::vector image; - LoadFlip(filename, vfs, image, w, h); + LoadFlip(filename, default_provider, image, w, h); // assign size width = w; @@ -2621,7 +2595,7 @@ void mjCTexture::Load2D(string filename, const mjVFS* vfs) { // load cube or skybox from single file (repeated or grid) -void mjCTexture::LoadCubeSingle(string filename, const mjVFS* vfs) { +void mjCTexture::LoadCubeSingle(string filename, int default_provider) { // check gridsize if (gridsize[0]<1 || gridsize[1]<1 || gridsize[0]*gridsize[1]>12) { throw mjCError(this, @@ -2632,7 +2606,7 @@ void mjCTexture::LoadCubeSingle(string filename, const mjVFS* vfs) { // load PNG or custom unsigned int w, h; std::vector image; - LoadFlip(filename, vfs, image, w, h); + LoadFlip(filename, default_provider, image, w, h); // check gridsize for compatibility if (w/gridsize[1]!=h/gridsize[0] || (w%gridsize[1]) || (h%gridsize[0])) { @@ -2721,7 +2695,7 @@ void mjCTexture::LoadCubeSingle(string filename, const mjVFS* vfs) { // load cube or skybox from separate file -void mjCTexture::LoadCubeSeparate(const mjVFS* vfs) { +void mjCTexture::LoadCubeSeparate(int default_provider) { // keep track of which faces were defined int loaded[6] = {0, 0, 0, 0, 0, 0}; @@ -2739,7 +2713,7 @@ void mjCTexture::LoadCubeSeparate(const mjVFS* vfs) { // load PNG or custom unsigned int w, h; std::vector image; - LoadFlip(filename, vfs, image, w, h); + LoadFlip(filename, default_provider, image, w, h); // PNG must be square if (w!=h) { @@ -2793,7 +2767,7 @@ void mjCTexture::LoadCubeSeparate(const mjVFS* vfs) { // compiler -void mjCTexture::Compile(const mjVFS* vfs) { +void mjCTexture::Compile(int default_provider) { // builtin if (builtin!=mjBUILTIN_NONE) { // check size @@ -2836,9 +2810,9 @@ void mjCTexture::Compile(const mjVFS* vfs) { // dispatch if (type==mjTEXTURE_2D) { - Load2D(filename, vfs); + Load2D(filename, default_provider); } else { - LoadCubeSingle(filename, vfs); + LoadCubeSingle(filename, default_provider); } } @@ -2866,7 +2840,7 @@ void mjCTexture::Compile(const mjVFS* vfs) { } // only cube and skybox - LoadCubeSeparate(vfs); + LoadCubeSeparate(default_provider); } // make sure someone allocated data; SHOULD NOT OCCUR diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 450caf57..bc90708a 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -512,10 +512,10 @@ class mjCMesh: public mjCBase { private: mjCMesh(mjCModel* = 0, mjCDef* = 0); // constructor ~mjCMesh(); // destructor - void Compile(const mjVFS* vfs); // compiler - void LoadOBJ(const mjVFS* vfs); // load mesh in wavefront OBJ format - void LoadSTL(const mjVFS* vfs); // load mesh in STL BIN format - void LoadMSH(const mjVFS* vfs); // load mesh in MSH BIN format + void Compile(int default_provider); // compiler + void LoadOBJ(int default_provider); // load mesh in wavefront OBJ format + void LoadSTL(int default_provider); // load mesh in STL BIN format + void LoadMSH(int default_provider); // load mesh in MSH BIN format void MakeGraph(void); // make graph of convex hull void CopyGraph(void); // copy graph into face data void MakeNormal(void); // compute vertex normals @@ -590,8 +590,8 @@ class mjCSkin: public mjCBase { private: mjCSkin(mjCModel* = 0); // constructor ~mjCSkin(); // destructor - void Compile(const mjVFS* vfs); // compiler - void LoadSKN(const mjVFS* vfs); // load skin in SKN BIN format + void Compile(int default_provider); // compiler + void LoadSKN(int default_provider); // load skin in SKN BIN format int matid; // material id std::vector bodyid; // body ids @@ -616,10 +616,10 @@ class mjCHField : public mjCBase { private: mjCHField(mjCModel* model); // constructor ~mjCHField(); // destructor - void Compile(const mjVFS* vfs); // compiler + void Compile(int default_provider); // compiler - void LoadCustom(std::string filename, const mjVFS* vfs); // load from custom format - void LoadPNG(std::string filename, const mjVFS* vfs); // load from PNG format + void LoadCustom(std::string filename, int default_provider); // load from custom format + void LoadPNG(std::string filename, int default_provider); // load from PNG format }; @@ -660,22 +660,22 @@ class mjCTexture : public mjCBase { private: mjCTexture(mjCModel*); // constructor ~mjCTexture(); // destructior - void Compile(const mjVFS* vfs); // compiler + void Compile(int default_provider); // compiler void Builtin2D(void); // make builtin 2D void BuiltinCube(void); // make builtin cube - void Load2D(std::string filename, const mjVFS* vfs); // load 2D from file - void LoadCubeSingle(std::string filename, const mjVFS* vfs); // load cube from single file - void LoadCubeSeparate(const mjVFS* vfs); // load cube from separate files + void Load2D(std::string filename, int default_provider); // load 2D from file + void LoadCubeSingle(std::string filename, int default_provider); // load cube from single file + void LoadCubeSeparate(int default_provider); // load cube from separate files - void LoadFlip(std::string filename, const mjVFS* vfs, // load and flip + void LoadFlip(std::string filename, int default_provider, // load and flip std::vector& image, unsigned int& w, unsigned int& h); - void LoadPNG(std::string filename, const mjVFS* vfs, + void LoadPNG(std::string filename, int default_provider, std::vector& image, unsigned int& w, unsigned int& h); - void LoadCustom(std::string filename, const mjVFS* vfs, + void LoadCustom(std::string filename, int default_provider, std::vector& image, unsigned int& w, unsigned int& h); diff --git a/src/xml/xml.cc b/src/xml/xml.cc index 7f6e539d..c68c3ea0 100644 --- a/src/xml/xml.cc +++ b/src/xml/xml.cc @@ -24,7 +24,7 @@ #include "cc/array_safety.h" #include "engine/engine_crossplatform.h" -#include "engine/engine_vfs.h" +#include "engine/engine_resource.h" #include "user/user_model.h" #include "user/user_util.h" #include "xml/xml_native_reader.h" @@ -129,7 +129,7 @@ bool mjWriteXML(mjCModel* model, string filename, char* error, int error_sz) { // find include elements recursively, replace them with subtree from xml file static XMLElement* mjIncludeXML(XMLElement* elem, string dir, - const mjVFS* vfs, vector& included) { + int default_provider, vector& included) { // include element: process if (!strcasecmp(elem->Value(), "include")) { // make sure include has no children @@ -150,23 +150,30 @@ static XMLElement* mjIncludeXML(XMLElement* elem, string dir, } // get data source - const char* xmlstring = 0; - int buffer_size = 0; - if (vfs) { - int id = mj_findFileVFS(vfs, filename.c_str()); - if (id>=0) { - xmlstring = (const char*)vfs->filedata[id]; - buffer_size = vfs->filesize[id]; + mjResource *resource = nullptr; + const char* xmlstring = nullptr; + if ((resource = mju_openResource(filename.c_str(), default_provider)) == nullptr) { + // load from OS filesystem + if (!default_provider || (resource = mju_openResource(filename.c_str(), 0)) == nullptr) { + throw mjXError(elem, "Could not open file '%s'", filename.c_str()); } } + int buffer_size = mju_readResource(resource, (const void**) &xmlstring); + if (buffer_size < 0) { + mju_closeResource(resource); + throw mjXError(elem, "Error reading file '%s'", filename.c_str()); + } else if (!buffer_size) { + mju_closeResource(resource); + throw mjXError(elem, "Empty file '%s'", filename.c_str()); + } + // load XML file or parse string XMLDocument doc; - if (xmlstring) { - doc.Parse(xmlstring, buffer_size); - } else { - doc.LoadFile(filename.c_str()); - } + doc.Parse(xmlstring, buffer_size); + + // close resource + mju_closeResource(resource); // check error if (doc.Error()) { @@ -208,27 +215,26 @@ static XMLElement* mjIncludeXML(XMLElement* elem, string dir, } // run XMLInclude on first new child - return mjIncludeXML(first->ToElement(), dir, vfs, included); + return mjIncludeXML(first->ToElement(), dir, default_provider, included); } // otherwise check all child elements, return self else { XMLElement* child = elem->FirstChildElement(); while (child) { - child = mjIncludeXML(child, dir, vfs, included); + child = mjIncludeXML(child, dir, default_provider, included); if (child) { child = child->NextSiblingElement(); } } - return elem; } } -// Main parser function: from file or VFS -mjCModel* mjParseXML(const char* filename, const mjVFS* vfs, char* error, int error_sz) { +// Main parser function +mjCModel* mjParseXML(const char* filename, int default_provider, char* error, int error_sz) { LocaleOverride locale_override; // check arguments @@ -236,33 +242,51 @@ mjCModel* mjParseXML(const char* filename, const mjVFS* vfs, char* error, int er if (error) { snprintf(error, error_sz, "mjParseXML: filename argument required\n"); } - return 0; + return nullptr; } // clear mjCModel* model = 0; if (error) { - error[0] = 0; + error[0] = '\0'; } // get data source - const char* xmlstring = 0; - int buffer_size = 0; - if (vfs) { - int id = mj_findFileVFS(vfs, filename); - if (id>=0) { - xmlstring = (const char*)vfs->filedata[id]; - buffer_size = vfs->filesize[id]; + mjResource* resource = nullptr; + const char* xmlstring = nullptr; + if ((resource = mju_openResource(filename, default_provider)) == nullptr) { + // load from OS filesystem + if (!default_provider || (resource = mju_openResource(filename, 0)) == nullptr) { + if (error) { + snprintf(error, error_sz, "mjParseXML: could not open file '%s'", filename); + } + return nullptr; } } + int buffer_size = mju_readResource(resource, (const void**) &xmlstring); + if (buffer_size < 0) { + if (error) { + snprintf(error, error_sz, "mjParseXML: error reading file '%s'", filename); + } + mju_closeResource(resource); + return nullptr; + } else if (!buffer_size) { + if (error) { + snprintf(error, error_sz, "mjParseXML: empty file '%s'", filename); + } + mju_closeResource(resource); + return nullptr; + } + + // load XML file or parse string XMLDocument doc; - if (xmlstring) { - doc.Parse(xmlstring, buffer_size); - } else { - doc.LoadFile(filename); - } + doc.Parse(xmlstring, buffer_size); + + // close resource + mju_closeResource(resource); + // error checking if (doc.Error()) { @@ -270,14 +294,14 @@ mjCModel* mjParseXML(const char* filename, const mjVFS* vfs, char* error, int er snprintf(error, error_sz, "XML parse error %d:\n%s\n", doc.ErrorID(), doc.ErrorStr()); } - return 0; + return nullptr; } // get top-level element XMLElement* root = doc.RootElement(); if (!root) { mjCopyError(error, "XML root element not found", error_sz); - return 0; + return nullptr; } // create model, set filedir @@ -290,7 +314,7 @@ mjCModel* mjParseXML(const char* filename, const mjVFS* vfs, char* error, int er // find include elements, replace them with subtree from xml file vector included; included.push_back(filename); - mjIncludeXML(root, model->modelfiledir, vfs, included); + mjIncludeXML(root, model->modelfiledir, default_provider, included); // parse MuJoCo model mjXReader parser; @@ -314,7 +338,7 @@ mjCModel* mjParseXML(const char* filename, const mjVFS* vfs, char* error, int er catch (mjXError err) { mjCopyError(error, err.message, error_sz); delete model; - return 0; + return nullptr; } return model; diff --git a/src/xml/xml.h b/src/xml/xml.h index 3bd08710..345135be 100644 --- a/src/xml/xml.h +++ b/src/xml/xml.h @@ -25,8 +25,8 @@ // Main writer function bool mjWriteXML(mjCModel* model, std::string filename, char* error, int error_sz); -// Main parser function: from file or VFS -mjCModel* mjParseXML(const char* filename, const mjVFS* vfs, char* error, int error_sz); +// Main parser function +mjCModel* mjParseXML(const char* filename, int default_provider, char* error, int error_sz); #endif // MUJOCO_SRC_XML_XML_H_ diff --git a/src/xml/xml_api.cc b/src/xml/xml_api.cc index 48497508..ff0a0daf 100644 --- a/src/xml/xml_api.cc +++ b/src/xml/xml_api.cc @@ -20,6 +20,8 @@ #include #include +#include "engine/engine_resource.h" +#include "engine/engine_vfs.h" #include "user/user_model.h" #include "xml/xml.h" #include "xml/xml_native_reader.h" @@ -80,26 +82,24 @@ void mj_deactivate(void) { -// parse XML file in MJCF or URDF format, compile it, return low-level model -// if vfs is not NULL, look up files in vfs before reading from disk -// error can be NULL; otherwise assumed to have size error_sz -mjModel* mj_loadXML(const char* filename, const mjVFS* vfs, - char* error, int error_sz) { +// mj_loadXML helper function +mjModel* _loadXML(const char* filename, int default_provider, + char* error, int error_sz) { // serialize access to themodel std::lock_guard lock(themutex); // parse new model - mjCModel* newmodel = mjParseXML(filename, vfs, error, error_sz); + mjCModel* newmodel = mjParseXML(filename, default_provider, error, error_sz); if (!newmodel) { - return 0; + return nullptr; } // compile new model - mjModel* m = newmodel->Compile(vfs); + mjModel* m = newmodel->Compile(default_provider); if (!m) { mjCopyError(error, newmodel->GetError().message, error_sz); delete newmodel; - return 0; + return nullptr; } // clear old and assign new @@ -110,7 +110,7 @@ mjModel* mj_loadXML(const char* filename, const mjVFS* vfs, if (themodel.model->GetError().warning) { mjCopyError(error, themodel.model->GetError().message, error_sz); } else if (error) { - error[0] = 0; + error[0] = '\0'; } return m; @@ -118,6 +118,30 @@ mjModel* mj_loadXML(const char* filename, const mjVFS* vfs, +// parse XML file in MJCF or URDF format, compile it, return low-level model +// if vfs is not NULL, look up files in vfs before reading from disk +// error can be NULL; otherwise assumed to have size error_sz +mjModel* mj_loadXML(const char* filename, const mjVFS* vfs, + char* error, int error_sz) { + + if (vfs == nullptr) { + return _loadXML(filename, 0, error, error_sz); + } + + int index = mj_registerVfsProvider(vfs); + if (index < 1) { + if (error) { + snprintf(error, error_sz, "mj_loadXML: could not register VFS"); + } + return nullptr; + } + + mjModel* model = _loadXML(filename, index, error, error_sz); + mjp_unregisterResourceProvider(index); + return model; +} + + // update XML data structures with info from low-level model, save as MJCF // returns 1 if successful, 0 otherwise diff --git a/test/engine/CMakeLists.txt b/test/engine/CMakeLists.txt index 3b14aa3c..d43a6fe7 100644 --- a/test/engine/CMakeLists.txt +++ b/test/engine/CMakeLists.txt @@ -55,6 +55,9 @@ target_link_libraries(engine_print_test fixture gmock) mujoco_test(engine_ray_test) target_link_libraries(engine_ray_test fixture gmock) +mujoco_test(engine_resource_test) +target_link_libraries(engine_resource_test fixture gmock) + mujoco_test(engine_sensor_test) target_link_libraries(engine_sensor_test fixture gmock) diff --git a/test/engine/engine_resource_test.cc b/test/engine/engine_resource_test.cc new file mode 100644 index 00000000..c596f71a --- /dev/null +++ b/test/engine/engine_resource_test.cc @@ -0,0 +1,227 @@ +// Copyright 2023 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. + +// Tests for engine/engine_resource.c + +#include + +#include +#include +#include "src/cc/array_safety.h" +#include +#include +#include +#include "src/engine/engine_plugin.h" +#include "src/engine/engine_resource.h" +#include "test/fixture.h" + +namespace mujoco { +namespace { + +using ::testing::HasSubstr; +using ::testing::IsNull; +using ::testing::NotNull; +using ::testing::StrEq; + +using ResourceTest = MujocoTest; + +int open_nop(mjResource* resource) { + return 1; +} + +int open_str(mjResource* resource) { + if (std::strcmp(resource->name, "str://file")) { + return 0; + } + + resource->data = mju_malloc(100*sizeof(char)); + std::strcpy((char*) resource->data, "Hello World"); + return 1; +} + +int read_nop(mjResource* resource, const void** buffer) { + return 0; +} + +int read_str(mjResource* resource, const void** buffer) { + *buffer = resource->data; + return std::strlen((const char*) resource->data) + 1; +} + +void close_nop(mjResource* resource) { +} + +void close_str(mjResource* resource) { + mju_free(resource->data); +} + +TEST_F(ResourceTest, RegisterProviderSuccess) { + mjpResourceProvider provider = {.prefix = "myprefix", + .open = open_nop, .read = read_nop, .close = close_nop}; + + int count1 = mjp_resourceProviderCount(); + int i = mjp_registerResourceProvider(&provider); + int count2 = mjp_resourceProviderCount(); + + EXPECT_GT(i, 0); + EXPECT_EQ(count1+1, count2); +} + +TEST_F(ResourceTest, RegisterProviderMissingCallbacks) { + mjpResourceProvider provider = {.prefix = "myprefix"}; + + // install warning handler + static char warning[1024]; + warning[0] = '\0'; + mju_user_warning = [](const char* msg) { + util::strcpy_arr(warning, msg); + }; + + int i = mjp_registerResourceProvider(&provider); + + // warning message related to missing callbacks + EXPECT_THAT(warning, HasSubstr("callback")); + EXPECT_LT(i, 1); +} + +TEST_F(ResourceTest, RegisterProviderMissingPrefix) { + mjpResourceProvider provider = {.prefix = "", + .open = open_nop, .read = read_nop, .close = close_nop}; + + // install warning handler + static char warning[1024]; + warning[0] = '\0'; + mju_user_warning = [](const char* msg) { + util::strcpy_arr(warning, msg); + }; + + int i = mjp_registerResourceProvider(&provider); + + // warning message related to missing prefix + EXPECT_THAT(warning, HasSubstr("prefix")); + EXPECT_LT(i, 1); +} + +TEST_F(ResourceTest, RegisterProviderSubPrefix) { + mjpResourceProvider provider = {.prefix = "prefix", + .open = open_nop, .read = read_nop, .close = close_nop}; + + mjpResourceProvider provider2 = {.prefix = "pre", + .open = open_nop, .read = read_nop, .close = close_nop}; + + // install warning handler + static char warning[1024]; + warning[0] = '\0'; + mju_user_warning = [](const char* msg) { + util::strcpy_arr(warning, msg); + }; + + int i = mjp_registerResourceProvider(&provider); + int j = mjp_registerResourceProvider(&provider2); + + + // warning message related to an error + EXPECT_THAT(warning, HasSubstr("cannot be register")); + EXPECT_GT(i, 0); + EXPECT_LT(j, 1); +} + +TEST_F(ResourceTest, RegisterProviderSuperPrefix) { + mjpResourceProvider provider = {.prefix = "prefix", + .open = open_nop, .read = read_nop, .close = close_nop}; + + mjpResourceProvider provider2 = {.prefix = "prefix2", + .open = open_nop, .read = read_nop, .close = close_nop}; + + // install warning handler + static char warning[1024]; + warning[0] = '\0'; + mju_user_warning = [](const char* msg) { + util::strcpy_arr(warning, msg); + }; + + int i = mjp_registerResourceProvider(&provider); + int j = mjp_registerResourceProvider(&provider2); + + + // warning message related to an error + EXPECT_THAT(warning, HasSubstr("cannot be register")); + EXPECT_GT(i, 0); + EXPECT_LT(j, 1); +} + +TEST_F(ResourceTest, RegisterProviderSame) { + mjpResourceProvider provider = {.prefix = "prefix", + .open = open_nop, .read = read_nop, .close = close_nop}; + + mjpResourceProvider provider2 = {.prefix = "prefix", + .open = open_nop, .read = read_nop, .close = close_nop}; + + int i1 = mjp_registerResourceProvider(&provider); + int count1 = mjp_resourceProviderCount(); + + int i2 = mjp_registerResourceProvider(&provider2); + int count2 = mjp_resourceProviderCount(); + + EXPECT_EQ(i1, i2); + EXPECT_EQ(count1, count2); + +} + +TEST_F(ResourceTest, GeneralTest) { + mjpResourceProvider provider = {.prefix = "str://", + .open = open_str, .read = read_str, .close = close_str}; + + // register resource provider + int i = mjp_registerResourceProvider(&provider); + EXPECT_GT(i, 0); + + // open resource + mjResource* resource = mju_openResource("str://file", 0); + ASSERT_THAT(resource, NotNull()); + + const char* buffer = NULL; + int bytes = mju_readResource(resource, (const void**) &buffer); + EXPECT_EQ(bytes, std::strlen("Hello World") + 1); + EXPECT_THAT(buffer, StrEq("Hello World")); + + mju_closeResource(resource); + +} + +TEST_F(ResourceTest, GeneralTestFailure) { + mjpResourceProvider provider = {.prefix = "str://", + .open = open_str, .read = read_str, .close = close_str}; + + // register resource provider + int i = mjp_registerResourceProvider(&provider); + EXPECT_GT(i, 0); + + + // install warning handler + static char warning[1024]; + warning[0] = '\0'; + mju_user_warning = [](const char* msg) { + util::strcpy_arr(warning, msg); + }; + + // open resource + mjResource* resource = mju_openResource("str://notfound", 0); + ASSERT_THAT(resource, IsNull()); + + EXPECT_THAT(warning, HasSubstr("could not open")); +} + +} // namespace +} // namespace mujoco