Add pluggable resource writing to MuJoCo
Extend mjpResourceProvider with an optional write callback (write) so that mj_encode, mj_saveXML, and mj_saveModel can write to any registered provider. PiperOrigin-RevId: 945202741 Change-Id: I37903425260932e555f4a8c2392c4ff8c2e6cc06
This commit is contained in:
committed by
Copybara-Service
parent
bdeb7e7c4d
commit
dc7581acfa
@@ -1639,6 +1639,17 @@ Close a resource; no-op if resource is NULL.
|
|||||||
Set buffer to bytes read from the resource and return number of bytes in buffer;
|
Set buffer to bytes read from the resource and return number of bytes in buffer;
|
||||||
return negative value if error.
|
return negative value if error.
|
||||||
|
|
||||||
|
.. _mju_writeResource:
|
||||||
|
|
||||||
|
`mju_writeResource <#mju_writeResource>`__
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. mujoco-include:: mju_writeResource
|
||||||
|
|
||||||
|
Write resource data via its resource provider, return bytes written or -1 on error.
|
||||||
|
|
||||||
|
*Nullable:* ``vfs``, ``error``
|
||||||
|
|
||||||
.. _mju_getResourceDir:
|
.. _mju_getResourceDir:
|
||||||
|
|
||||||
`mju_getResourceDir <#mju_getResourceDir>`__
|
`mju_getResourceDir <#mju_getResourceDir>`__
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ General
|
|||||||
- Fixed loading of ``.mjz`` archives in :ref:`simulate<saSimulate>`: the archive was unmounted before model compilation,
|
- Fixed loading of ``.mjz`` archives in :ref:`simulate<saSimulate>`: the archive was unmounted before model compilation,
|
||||||
so assets contained in it failed to load. Failures in the ``mjz`` decoder now emit a warning with the underlying
|
so assets contained in it failed to load. Failures in the ``mjz`` decoder now emit a warning with the underlying
|
||||||
error instead of the generic "could not decode content" message.
|
error instead of the generic "could not decode content" message.
|
||||||
|
- Added support for resource writing via :ref:`mju_writeResource` and the ``write`` callback in :ref:`mjpResourceProvider`.
|
||||||
|
|
||||||
.. admonition:: Breaking API changes
|
.. admonition:: Breaking API changes
|
||||||
:class: attention
|
:class: attention
|
||||||
|
|||||||
@@ -1218,6 +1218,7 @@ typedef struct mjpResourceProvider {
|
|||||||
mjfMountResource mount; // mounting callback (optional)
|
mjfMountResource mount; // mounting callback (optional)
|
||||||
mjfUnmountResource unmount; // unmounting callback (optional)
|
mjfUnmountResource unmount; // unmounting callback (optional)
|
||||||
mjfResourceModified modified; // resource modified callback (optional)
|
mjfResourceModified modified; // resource modified callback (optional)
|
||||||
|
mjfWriteResource write; // writing callback (optional)
|
||||||
void* data; // opaque data pointer (resource invariant)
|
void* data; // opaque data pointer (resource invariant)
|
||||||
} mjpResourceProvider;
|
} mjpResourceProvider;
|
||||||
typedef struct mjpDecoder {
|
typedef struct mjpDecoder {
|
||||||
@@ -3684,6 +3685,8 @@ mjResource* mju_openResource(const char* dir, const char* name,
|
|||||||
const mjVFS* vfs, char* error, size_t nerror);
|
const mjVFS* vfs, char* error, size_t nerror);
|
||||||
void mju_closeResource(mjResource* resource);
|
void mju_closeResource(mjResource* resource);
|
||||||
int mju_readResource(mjResource* resource, const void** buffer);
|
int mju_readResource(mjResource* resource, const void** buffer);
|
||||||
|
mjtSize mju_writeResource(const char* name, const void* buffer, mjtSize nbytes,
|
||||||
|
const mjVFS* vfs, char* error, size_t nerror);
|
||||||
void mju_getResourceDir(mjResource* resource, const char** dir, int* ndir);
|
void mju_getResourceDir(mjResource* resource, const char** dir, int* ndir);
|
||||||
int mju_isModifiedResource(const mjResource* resource, const char* timestamp);
|
int mju_isModifiedResource(const mjResource* resource, const char* timestamp);
|
||||||
mjSpec* mju_decodeResource(mjResource* resource, const char* content_type,
|
mjSpec* mju_decodeResource(mjResource* resource, const char* content_type,
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ typedef int (*mjfUnmountResource)(mjResource* resource);
|
|||||||
// returns < 0 if the resource is older than the given timestamp
|
// returns < 0 if the resource is older than the given timestamp
|
||||||
typedef int (*mjfResourceModified)(const mjResource* resource, const char* timestamp);
|
typedef int (*mjfResourceModified)(const mjResource* resource, const char* timestamp);
|
||||||
|
|
||||||
|
// callback for writing bytes to a resource
|
||||||
|
// return number of bytes written, return -1 if error
|
||||||
|
typedef mjtSize (*mjfWriteResource)(mjResource* resource, const void* buffer, mjtSize nbytes);
|
||||||
|
|
||||||
// struct describing a single resource provider
|
// struct describing a single resource provider
|
||||||
typedef struct mjpResourceProvider {
|
typedef struct mjpResourceProvider {
|
||||||
const char* prefix; // prefix for match against a resource name
|
const char* prefix; // prefix for match against a resource name
|
||||||
@@ -67,6 +71,7 @@ typedef struct mjpResourceProvider {
|
|||||||
mjfMountResource mount; // mounting callback (optional)
|
mjfMountResource mount; // mounting callback (optional)
|
||||||
mjfUnmountResource unmount; // unmounting callback (optional)
|
mjfUnmountResource unmount; // unmounting callback (optional)
|
||||||
mjfResourceModified modified; // resource modified callback (optional)
|
mjfResourceModified modified; // resource modified callback (optional)
|
||||||
|
mjfWriteResource write; // writing callback (optional)
|
||||||
void* data; // opaque data pointer (resource invariant)
|
void* data; // opaque data pointer (resource invariant)
|
||||||
} mjpResourceProvider;
|
} mjpResourceProvider;
|
||||||
|
|
||||||
|
|||||||
@@ -1588,6 +1588,11 @@ MJAPI void mju_closeResource(mjResource* resource);
|
|||||||
// return negative value if error.
|
// return negative value if error.
|
||||||
MJAPI int mju_readResource(mjResource* resource, const void** buffer);
|
MJAPI int mju_readResource(mjResource* resource, const void** buffer);
|
||||||
|
|
||||||
|
// Write resource data via its resource provider, return bytes written or -1 on error.
|
||||||
|
// Nullable: vfs, error
|
||||||
|
MJAPI mjtSize mju_writeResource(const char* name, const void* buffer, mjtSize nbytes,
|
||||||
|
const mjVFS* vfs, char* error, size_t nerror);
|
||||||
|
|
||||||
// For a resource with a name partitioned as {dir}{filename}, get the dir and ndir pointers.
|
// For a resource with a name partitioned as {dir}{filename}, get the dir and ndir pointers.
|
||||||
MJAPI void mju_getResourceDir(mjResource* resource, const char** dir, int* ndir);
|
MJAPI void mju_getResourceDir(mjResource* resource, const char** dir, int* ndir);
|
||||||
|
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ _OMITTED_FUNCTIONS = [
|
|||||||
'mju_openResource',
|
'mju_openResource',
|
||||||
'mju_closeResource',
|
'mju_closeResource',
|
||||||
'mju_readResource',
|
'mju_readResource',
|
||||||
|
'mju_writeResource',
|
||||||
'mju_getResourceDir',
|
'mju_getResourceDir',
|
||||||
'mju_isModifiedResource',
|
'mju_isModifiedResource',
|
||||||
'mj_compile',
|
'mj_compile',
|
||||||
|
|||||||
@@ -9929,6 +9929,48 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
|
|||||||
),
|
),
|
||||||
doc='Set buffer to bytes read from the resource and return number of bytes in buffer; return negative value if error.', # pylint: disable=line-too-long
|
doc='Set buffer to bytes read from the resource and return number of bytes in buffer; return negative value if error.', # pylint: disable=line-too-long
|
||||||
)),
|
)),
|
||||||
|
('mju_writeResource',
|
||||||
|
FunctionDecl(
|
||||||
|
name='mju_writeResource',
|
||||||
|
return_type=ValueType(name='mjtSize'),
|
||||||
|
parameters=(
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='name',
|
||||||
|
type=PointerType(
|
||||||
|
inner_type=ValueType(name='char', is_const=True),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='buffer',
|
||||||
|
type=PointerType(
|
||||||
|
inner_type=ValueType(name='void', is_const=True),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='nbytes',
|
||||||
|
type=ValueType(name='mjtSize'),
|
||||||
|
),
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='vfs',
|
||||||
|
type=PointerType(
|
||||||
|
inner_type=ValueType(name='mjVFS', is_const=True),
|
||||||
|
),
|
||||||
|
nullable=True,
|
||||||
|
),
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='error',
|
||||||
|
type=PointerType(
|
||||||
|
inner_type=ValueType(name='char'),
|
||||||
|
),
|
||||||
|
nullable=True,
|
||||||
|
),
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='nerror',
|
||||||
|
type=ValueType(name='size_t'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
doc='Write resource data via its resource provider, return bytes written or -1 on error.', # pylint: disable=line-too-long
|
||||||
|
)),
|
||||||
('mju_getResourceDir',
|
('mju_getResourceDir',
|
||||||
FunctionDecl(
|
FunctionDecl(
|
||||||
name='mju_getResourceDir',
|
name='mju_getResourceDir',
|
||||||
|
|||||||
+25
-38
@@ -503,56 +503,43 @@ void mjv_copyModel(mjModel* dest, const mjModel* src) {
|
|||||||
|
|
||||||
// save model to binary file, or memory buffer of szbuf>0
|
// save model to binary file, or memory buffer of szbuf>0
|
||||||
void mj_saveModel(const mjModel* m, const char* filename, void* buffer, int buffer_sz) {
|
void mj_saveModel(const mjModel* m, const char* filename, void* buffer, int buffer_sz) {
|
||||||
FILE* fp = 0;
|
|
||||||
mjtSize ptrbuf = 0;
|
mjtSize ptrbuf = 0;
|
||||||
|
|
||||||
// standard header
|
// standard header
|
||||||
int header[NHEADER] = {ID, sizeof(mjtNum), getnsize(), mj_version(), getnptr()};
|
int header[NHEADER] = {ID, sizeof(mjtNum), getnsize(), mj_version(), getnptr()};
|
||||||
|
|
||||||
// open file for writing if no buffer
|
// no buffer: serialize to temporary buffer, then write via resource provider
|
||||||
if (!buffer) {
|
if (!buffer) {
|
||||||
fp = fopen(filename, "wb");
|
mjtSize sz = mj_sizeModel(m);
|
||||||
if (!fp) {
|
void* tmpbuf = mju_malloc(sz);
|
||||||
mju_warning("Could not open file '%s'", filename);
|
if (!tmpbuf) {
|
||||||
|
mju_warning("Could not allocate buffer for saving model");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
mj_saveModel(m, NULL, tmpbuf, (int)sz);
|
||||||
|
|
||||||
|
mjtSize written = mju_writeResource(filename, tmpbuf, sz, NULL, NULL, 0);
|
||||||
|
if (written != sz) {
|
||||||
|
mju_warning("Could not save model to '%s'", filename);
|
||||||
|
}
|
||||||
|
mju_free(tmpbuf);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// write standard header, info, options, buffer (omit pointers)
|
// write standard header, info, options, buffer (omit pointers)
|
||||||
if (fp) {
|
bufwrite(header, sizeof(header), buffer_sz, buffer, &ptrbuf);
|
||||||
fwrite(header, sizeof(int), NHEADER, fp);
|
#define X(name) bufwrite(&m->name, sizeof(m->name), buffer_sz, buffer, &ptrbuf);
|
||||||
#define X(name) fwrite(&m->name, sizeof(m->name), 1, fp);
|
MJMODEL_SIZES
|
||||||
MJMODEL_SIZES
|
#undef X
|
||||||
|
bufwrite((void*)&m->opt, sizeof(mjOption), buffer_sz, buffer, &ptrbuf);
|
||||||
|
bufwrite((void*)&m->vis, sizeof(mjVisual), buffer_sz, buffer, &ptrbuf);
|
||||||
|
bufwrite((void*)&m->stat, sizeof(mjStatistic), buffer_sz, buffer, &ptrbuf);
|
||||||
|
{
|
||||||
|
MJMODEL_POINTERS_PREAMBLE(m)
|
||||||
|
#define X(type, name, nr, nc) \
|
||||||
|
bufwrite((void*)m->name, sizeof(type)*(m->nr)*(nc), buffer_sz, buffer, &ptrbuf);
|
||||||
|
MJMODEL_POINTERS
|
||||||
#undef X
|
#undef X
|
||||||
fwrite((void*)&m->opt, sizeof(mjOption), 1, fp);
|
|
||||||
fwrite((void*)&m->vis, sizeof(mjVisual), 1, fp);
|
|
||||||
fwrite((void*)&m->stat, sizeof(mjStatistic), 1, fp);
|
|
||||||
{
|
|
||||||
MJMODEL_POINTERS_PREAMBLE(m)
|
|
||||||
#define X(type, name, nr, nc) \
|
|
||||||
fwrite((void*)m->name, sizeof(type), (m->nr)*(nc), fp);
|
|
||||||
MJMODEL_POINTERS
|
|
||||||
#undef X
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bufwrite(header, sizeof(header), buffer_sz, buffer, &ptrbuf);
|
|
||||||
#define X(name) bufwrite(&m->name, sizeof(m->name), buffer_sz, buffer, &ptrbuf);
|
|
||||||
MJMODEL_SIZES
|
|
||||||
#undef X
|
|
||||||
bufwrite((void*)&m->opt, sizeof(mjOption), buffer_sz, buffer, &ptrbuf);
|
|
||||||
bufwrite((void*)&m->vis, sizeof(mjVisual), buffer_sz, buffer, &ptrbuf);
|
|
||||||
bufwrite((void*)&m->stat, sizeof(mjStatistic), buffer_sz, buffer, &ptrbuf);
|
|
||||||
{
|
|
||||||
MJMODEL_POINTERS_PREAMBLE(m)
|
|
||||||
#define X(type, name, nr, nc) \
|
|
||||||
bufwrite((void*)m->name, sizeof(type)*(m->nr)*(nc), buffer_sz, buffer, &ptrbuf);
|
|
||||||
MJMODEL_POINTERS
|
|
||||||
#undef X
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fp) {
|
|
||||||
fclose(fp);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -249,11 +249,10 @@ std::string_view GlobalTable<mjpResourceProvider>::ObjectKey(const mjpResourcePr
|
|||||||
// check if two resource providers are identical
|
// check if two resource providers are identical
|
||||||
template <>
|
template <>
|
||||||
bool GlobalTable<mjpResourceProvider>::ObjectEqual(const mjpResourceProvider& p1, const mjpResourceProvider& p2) {
|
bool GlobalTable<mjpResourceProvider>::ObjectEqual(const mjpResourceProvider& p1, const mjpResourceProvider& p2) {
|
||||||
return (CaseInsensitiveEqual(p1.prefix, p2.prefix) &&
|
return (CaseInsensitiveEqual(p1.prefix, p2.prefix) && p1.open == p2.open &&
|
||||||
p1.open == p2.open &&
|
p1.read == p2.read && p1.close == p2.close &&
|
||||||
p1.read == p2.read &&
|
|
||||||
p1.close == p2.close &&
|
|
||||||
p1.modified == p2.modified &&
|
p1.modified == p2.modified &&
|
||||||
|
p1.write == p2.write &&
|
||||||
p1.data == p2.data);
|
p1.data == p2.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +389,8 @@ bool GlobalTable<mjpEncoder>::ObjectEqual(const mjpEncoder& e1,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return content_type_match && extension_match
|
return content_type_match && extension_match
|
||||||
&& e1.encode == e2.encode && e1.close_resource == e2.close_resource;
|
&& e1.encode == e2.encode
|
||||||
|
&& e1.close_resource == e2.close_resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
|
|||||||
+5
-16
@@ -261,29 +261,18 @@ mjtSize mj_encode(const mjSpec* s, const mjModel* m, const char* filename,
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
FILE* fp = fopen(filename, "wb");
|
mjtSize written = mju_writeResource(filename, resource.data, nbytes, vfs, error, error_sz);
|
||||||
if (!fp) {
|
|
||||||
std::free(resource.data);
|
|
||||||
if (error) {
|
|
||||||
strncpy(error, "could not open file for writing", error_sz);
|
|
||||||
error[error_sz - 1] = '\0';
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::size_t written = fwrite(resource.data, 1, nbytes, fp);
|
|
||||||
fclose(fp);
|
|
||||||
encoder->close_resource(&resource);
|
encoder->close_resource(&resource);
|
||||||
|
|
||||||
if (static_cast<mjtSize>(written) != nbytes) {
|
if (written != nbytes) {
|
||||||
if (error) {
|
if (error && error[0] == '\0') {
|
||||||
strncpy(error, "failed to write all bytes to file", error_sz);
|
strncpy(error, "write failed", error_sz);
|
||||||
error[error_sz - 1] = '\0';
|
error[error_sz - 1] = '\0';
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return nbytes;
|
return written;
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper function to log compile time diagnostics
|
// helper function to log compile time diagnostics
|
||||||
|
|||||||
+56
-10
@@ -30,8 +30,13 @@
|
|||||||
#include "user/user_util.h"
|
#include "user/user_util.h"
|
||||||
#include "user/user_vfs.h"
|
#include "user/user_vfs.h"
|
||||||
|
|
||||||
mjResource* mju_openResource(const char* dir, const char* name,
|
static mjResource* openResourceInternal(const char* dir, const char* name,
|
||||||
const mjVFS* vfs, char* error, size_t nerror) {
|
const mjVFS* vfs, char* error,
|
||||||
|
size_t nerror) {
|
||||||
|
if (error && nerror > 0) {
|
||||||
|
error[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Update API to use non-const pointer. Unfortunately, while this is
|
// TODO: Update API to use non-const pointer. Unfortunately, while this is
|
||||||
// ABI stable, it will cause compiler errors in user code that is const
|
// ABI stable, it will cause compiler errors in user code that is const
|
||||||
// correct.
|
// correct.
|
||||||
@@ -52,20 +57,21 @@ mjResource* mju_openResource(const char* dir, const char* name,
|
|||||||
non_const_vfs = local_vfs;
|
non_const_vfs = local_vfs;
|
||||||
}
|
}
|
||||||
|
|
||||||
mjResource* resource =
|
mjResource* resource = mujoco::user::VFS::Upcast(non_const_vfs)
|
||||||
mujoco::user::VFS::Upcast(non_const_vfs)->Open(dir ? dir : "", name);
|
->Open(dir ? dir : "", name, error, nerror);
|
||||||
|
|
||||||
if (error) {
|
if (error && nerror > 0 && !resource && error[0] == '\0') {
|
||||||
if (resource) {
|
std::snprintf(error, nerror, "Error opening file '%s'", name);
|
||||||
error[0] = '\0';
|
|
||||||
} else {
|
|
||||||
std::snprintf(error, nerror, "Error opening file '%s'", name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mjResource* mju_openResource(const char* dir, const char* name,
|
||||||
|
const mjVFS* vfs, char* error, size_t nerror) {
|
||||||
|
return openResourceInternal(dir, name, vfs, error, nerror);
|
||||||
|
}
|
||||||
|
|
||||||
void mju_closeResource(mjResource* resource) {
|
void mju_closeResource(mjResource* resource) {
|
||||||
if (resource && resource->vfs) {
|
if (resource && resource->vfs) {
|
||||||
mujoco::user::VFS::Upcast(resource->vfs)->Close(resource);
|
mujoco::user::VFS::Upcast(resource->vfs)->Close(resource);
|
||||||
@@ -79,6 +85,46 @@ int mju_readResource(mjResource* resource, const void** buffer) {
|
|||||||
return -1; // default (error reading bytes)
|
return -1; // default (error reading bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mjtSize mju_writeResource(const char* name, const void* buffer, mjtSize nbytes,
|
||||||
|
const mjVFS* vfs, char* error, size_t nerror) {
|
||||||
|
if (error && nerror > 0) {
|
||||||
|
error[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
if (error && nerror > 0) {
|
||||||
|
std::snprintf(error, nerror, "Resource name is NULL");
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
mjResource resource = {};
|
||||||
|
resource.name = const_cast<char*>(name);
|
||||||
|
|
||||||
|
mjVFS* non_const_vfs = const_cast<mjVFS*>(vfs);
|
||||||
|
bool local_vfs_created = false;
|
||||||
|
if (non_const_vfs == nullptr) {
|
||||||
|
non_const_vfs = (mjVFS*)mju_malloc(sizeof(mjVFS));
|
||||||
|
mj_defaultVFS(non_const_vfs);
|
||||||
|
local_vfs_created = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
resource.vfs = non_const_vfs;
|
||||||
|
|
||||||
|
mjtSize written = mujoco::user::VFS::Upcast(non_const_vfs)->Write(&resource, buffer, nbytes);
|
||||||
|
|
||||||
|
if (local_vfs_created) {
|
||||||
|
mj_deleteVFS(non_const_vfs);
|
||||||
|
mju_free(non_const_vfs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (written < 0 && error && nerror > 0 && error[0] == '\0') {
|
||||||
|
std::snprintf(error, nerror, "Error writing resource '%s'", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
|
||||||
void mju_getResourceDir(mjResource* resource, const char** dir, int* ndir) {
|
void mju_getResourceDir(mjResource* resource, const char** dir, int* ndir) {
|
||||||
*dir = nullptr;
|
*dir = nullptr;
|
||||||
*ndir = 0;
|
*ndir = 0;
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ MJAPI void mju_closeResource(mjResource* resource);
|
|||||||
// return negative value if error
|
// return negative value if error
|
||||||
MJAPI int mju_readResource(mjResource* resource, const void** buffer);
|
MJAPI int mju_readResource(mjResource* resource, const void** buffer);
|
||||||
|
|
||||||
|
MJAPI mjtSize mju_writeResource(const char* name, const void* buffer, mjtSize nbytes,
|
||||||
|
const mjVFS* vfs, char* error, size_t nerror);
|
||||||
|
|
||||||
// set for a resource with a name partitioned as {dir}{filename}, the dir and ndir pointers
|
// set for a resource with a name partitioned as {dir}{filename}, the dir and ndir pointers
|
||||||
MJAPI void mju_getResourceDir(mjResource* resource, const char** dir, int* ndir);
|
MJAPI void mju_getResourceDir(mjResource* resource, const char** dir, int* ndir);
|
||||||
|
|
||||||
|
|||||||
+39
-4
@@ -23,6 +23,7 @@
|
|||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
@@ -120,6 +121,15 @@ VFS::VFS(mjVFS* vfs) {
|
|||||||
default_provider_.modified = [](const mjResource* res, const char* time) {
|
default_provider_.modified = [](const mjResource* res, const char* time) {
|
||||||
return FileModified(res, time);
|
return FileModified(res, time);
|
||||||
};
|
};
|
||||||
|
default_provider_.write = [](mjResource* res, const void* buffer, mjtSize nbytes) -> mjtSize {
|
||||||
|
FILE* fp = fopen(res->name, "wb");
|
||||||
|
if (!fp) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
mjtSize written = static_cast<mjtSize>(fwrite(buffer, 1, nbytes, fp));
|
||||||
|
fclose(fp);
|
||||||
|
return written == nbytes ? written : -1;
|
||||||
|
};
|
||||||
|
|
||||||
default_provider_.prefix = nullptr;
|
default_provider_.prefix = nullptr;
|
||||||
default_mount_.vfs = &wrapped_vfs_;
|
default_mount_.vfs = &wrapped_vfs_;
|
||||||
@@ -149,22 +159,28 @@ VFS::~VFS() {
|
|||||||
mounts_.clear();
|
mounts_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
mjResource* VFS::Open(const char* dir, const char* name) {
|
mjResource* VFS::Open(const char* dir, const char* name, char* error,
|
||||||
|
size_t nerror) {
|
||||||
const std::string path = FilePath(dir, name).Str();
|
const std::string path = FilePath(dir, name).Str();
|
||||||
|
|
||||||
const mjResource* mount = FindMount(path);
|
const mjResource* mount = FindMount(path);
|
||||||
if (!mount) {
|
if (!mount || !mount->provider) {
|
||||||
|
if (error) {
|
||||||
|
std::snprintf(error, nerror, "No provider found for '%s'", name);
|
||||||
|
}
|
||||||
MaybeSelfDestruct();
|
MaybeSelfDestruct();
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResourcePtr res = CreateResource(path.c_str(), mount->provider);
|
const mjpResourceProvider* provider = mount->provider;
|
||||||
|
|
||||||
|
ResourcePtr res = CreateResource(path.c_str(), provider);
|
||||||
|
|
||||||
// Smuggle the mounted resource provider's mount-specific data pointer in the
|
// Smuggle the mounted resource provider's mount-specific data pointer in the
|
||||||
// requested resource's data pointer. This allows the provider to access its
|
// requested resource's data pointer. This allows the provider to access its
|
||||||
// own per-mount data without any intrusive changes to the provider interface.
|
// own per-mount data without any intrusive changes to the provider interface.
|
||||||
res->data = mount->data;
|
res->data = mount->data;
|
||||||
const int result = mount->provider->open(res.get());
|
const int result = provider->open ? provider->open(res.get()) : 0;
|
||||||
// If the data pointer was not modified, then that means the resource did not
|
// If the data pointer was not modified, then that means the resource did not
|
||||||
// set its own data pointer. So, we need to set it back to nullptr.
|
// set its own data pointer. So, we need to set it back to nullptr.
|
||||||
if (res->data == mount->data) {
|
if (res->data == mount->data) {
|
||||||
@@ -261,6 +277,25 @@ int VFS::Read(mjResource* resource, const void** buffer) {
|
|||||||
return kFailedToRead;
|
return kFailedToRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mjtSize VFS::Write(mjResource* resource, const void* buffer, mjtSize nbytes) {
|
||||||
|
if (resource) {
|
||||||
|
if (!resource->provider) {
|
||||||
|
const mjResource* mount = FindMount(resource->name);
|
||||||
|
if (mount) {
|
||||||
|
resource->provider = mount->provider;
|
||||||
|
// Smuggle mount data if not already set.
|
||||||
|
if (!resource->data) {
|
||||||
|
resource->data = mount->data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (resource->provider && resource->provider->write) {
|
||||||
|
return resource->provider->write(resource, buffer, nbytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
VFS::ResourcePtr VFS::CreateResource(std::string_view name,
|
VFS::ResourcePtr VFS::CreateResource(std::string_view name,
|
||||||
const mjpResourceProvider* provider) {
|
const mjpResourceProvider* provider) {
|
||||||
mjResource* res = new mjResource();
|
mjResource* res = new mjResource();
|
||||||
|
|||||||
+9
-3
@@ -72,15 +72,21 @@ class VFS {
|
|||||||
// Opens a mjResource for the given path, or nullptr on error. If successful,
|
// Opens a mjResource for the given path, or nullptr on error. If successful,
|
||||||
// will invoke the 'open' callback for the mjpResourceProvider associated with
|
// will invoke the 'open' callback for the mjpResourceProvider associated with
|
||||||
// the path/dir.
|
// the path/dir.
|
||||||
mjResource* Open(const char* dir, const char* name);
|
mjResource* Open(const char* dir, const char* name, char* error = nullptr,
|
||||||
|
size_t nerror = 0);
|
||||||
|
|
||||||
// Sets `buffer` to the contents of the resource and returns the number of
|
// Sets `buffer` to the contents of the resource and returns the number of
|
||||||
// bytes of the content. This is done by invoking the 'read' callback for the
|
// bytes of the content. This is done by invoking the 'read' callback for the
|
||||||
// mjpResourceProvider associated with the resource. Returns -1 on error.
|
// mjpResourceProvider associated with the resource. Returns -1 on error.
|
||||||
int Read(mjResource* resource, const void** buffer);
|
int Read(mjResource* resource, const void** buffer);
|
||||||
|
|
||||||
// Closes the resource by invoking the 'close' callback for the
|
// Writes the resource data. This is done by invoking the 'write' callback for
|
||||||
// mjpResourceProvider associated with the resource.
|
// the mjpResourceProvider associated with the resource.
|
||||||
|
// Returns bytes written, or -1 on error.
|
||||||
|
mjtSize Write(mjResource* resource, const void* buffer, mjtSize nbytes);
|
||||||
|
|
||||||
|
// Closes the resource by invoking the 'close' callback for
|
||||||
|
// the mjpResourceProvider associated with the resource.
|
||||||
Status Close(mjResource* resource);
|
Status Close(mjResource* resource);
|
||||||
|
|
||||||
// Mounts a ResourceProvider at the given path. All subsequent operations
|
// Mounts a ResourceProvider at the given path. All subsequent operations
|
||||||
|
|||||||
+7
-4
@@ -186,10 +186,13 @@ int mj_saveXML(const mjSpec* s, const char* filename, char* error, int error_sz)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ofstream file;
|
mjtSize written = mju_writeResource(filename, result.data(), result.size(), NULL, error, error_sz);
|
||||||
file.open(filename);
|
if (written != result.size()) {
|
||||||
file << result;
|
if (error && error_sz > 0 && error[0] == '\0') {
|
||||||
file.close();
|
std::snprintf(error, error_sz, "Error writing XML file '%s'", filename);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ mjtSize FakeEncode(const mjSpec* s, const mjModel* m, const mjVFS* vfs,
|
|||||||
|
|
||||||
void CloseResource(mjResource* resource) {
|
void CloseResource(mjResource* resource) {
|
||||||
delete static_cast<FakeEncoderOutput*>(resource->data);
|
delete static_cast<FakeEncoderOutput*>(resource->data);
|
||||||
delete resource;
|
resource->data = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
mjpEncoder FakeEncoder() {
|
mjpEncoder FakeEncoder() {
|
||||||
@@ -125,7 +125,7 @@ TEST_F(EncoderPluginTest, EncodeModel) {
|
|||||||
EXPECT_EQ(output->njnt, 0);
|
EXPECT_EQ(output->njnt, 0);
|
||||||
EXPECT_STREQ(output->resource_name, "output.fakeformat");
|
EXPECT_STREQ(output->resource_name, "output.fakeformat");
|
||||||
|
|
||||||
delete output;
|
found->close_resource(&resource);
|
||||||
mj_deleteModel(model);
|
mj_deleteModel(model);
|
||||||
mj_deleteSpec(spec);
|
mj_deleteSpec(spec);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,11 @@
|
|||||||
|
|
||||||
// Tests for user/user_resource.cc
|
// Tests for user/user_resource.cc
|
||||||
|
|
||||||
|
#include "src/user/user_resource.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -29,7 +32,6 @@
|
|||||||
#include <mujoco/mujoco.h>
|
#include <mujoco/mujoco.h>
|
||||||
#include "src/engine/engine_plugin.h"
|
#include "src/engine/engine_plugin.h"
|
||||||
#include "src/engine/engine_util_misc.h"
|
#include "src/engine/engine_util_misc.h"
|
||||||
#include "src/user/user_resource.h"
|
|
||||||
#include "test/fixture.h"
|
#include "test/fixture.h"
|
||||||
|
|
||||||
namespace mujoco {
|
namespace mujoco {
|
||||||
@@ -441,5 +443,106 @@ TEST_F(ResourceTest, OSFilesystemTimestamps) {
|
|||||||
mju_closeResource(resource);
|
mju_closeResource(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================== Write Resource Tests =====================
|
||||||
|
|
||||||
|
struct WriteBuffer {
|
||||||
|
std::vector<uint8_t> data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// static storage for the last written buffer (for round-trip testing)
|
||||||
|
static std::vector<uint8_t> last_written;
|
||||||
|
|
||||||
|
mjtSize write_capture(mjResource* resource, const void* buffer, mjtSize nbytes) {
|
||||||
|
last_written.clear();
|
||||||
|
const uint8_t* bytes = static_cast<const uint8_t*>(buffer);
|
||||||
|
last_written.insert(last_written.end(), bytes, bytes + nbytes);
|
||||||
|
return nbytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// open callback for reading back captured data
|
||||||
|
int open_read_capture(mjResource* resource) { return 1; }
|
||||||
|
|
||||||
|
int read_capture(mjResource* resource, const void** buffer) {
|
||||||
|
*buffer = last_written.data();
|
||||||
|
return static_cast<int>(last_written.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void close_read_capture(mjResource* resource) {}
|
||||||
|
|
||||||
|
TEST_F(ResourceTest, WriteResourceWithProvider) {
|
||||||
|
// Register a provider with write callbacks
|
||||||
|
mjpResourceProvider provider = {
|
||||||
|
.prefix = "wrtest",
|
||||||
|
.open = open_read_capture,
|
||||||
|
.read = read_capture,
|
||||||
|
.close = close_read_capture,
|
||||||
|
.write = write_capture,
|
||||||
|
};
|
||||||
|
|
||||||
|
mjp_registerResourceProvider(&provider);
|
||||||
|
|
||||||
|
// Write some data
|
||||||
|
const char* data = "Hello, Resource Writer!";
|
||||||
|
mjtSize nbytes = static_cast<mjtSize>(std::strlen(data));
|
||||||
|
|
||||||
|
mjtSize written = mju_writeResource("wrtest:myfile.txt", data, nbytes, nullptr, nullptr, 0);
|
||||||
|
EXPECT_EQ(written, nbytes);
|
||||||
|
|
||||||
|
// Read it back via the same provider
|
||||||
|
char error[256] = {0};
|
||||||
|
mjResource* resource =
|
||||||
|
mju_openResource("", "wrtest:myfile.txt", nullptr, error, sizeof(error));
|
||||||
|
ASSERT_THAT(resource, NotNull());
|
||||||
|
|
||||||
|
const void* buf = nullptr;
|
||||||
|
int read_bytes = mju_readResource(resource, &buf);
|
||||||
|
EXPECT_EQ(read_bytes, nbytes);
|
||||||
|
EXPECT_EQ(std::memcmp(buf, data, nbytes), 0);
|
||||||
|
|
||||||
|
mju_closeResource(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ResourceTest, WriteResourceDefaultPosix) {
|
||||||
|
// Write to a temp file using the default POSIX provider (no prefix)
|
||||||
|
std::string tmpfile = testing::TempDir() + "/mj_write_test.bin";
|
||||||
|
const char* data = "MuJoCo resource write test";
|
||||||
|
mjtSize nbytes = static_cast<mjtSize>(std::strlen(data));
|
||||||
|
|
||||||
|
mjtSize written = mju_writeResource(tmpfile.c_str(), data, nbytes, nullptr, nullptr, 0);
|
||||||
|
EXPECT_EQ(written, nbytes);
|
||||||
|
|
||||||
|
// Read it back via mju_openResource (default POSIX provider)
|
||||||
|
char error[256] = {0};
|
||||||
|
mjResource* resource =
|
||||||
|
mju_openResource("", tmpfile.c_str(), nullptr, error, sizeof(error));
|
||||||
|
ASSERT_THAT(resource, NotNull());
|
||||||
|
|
||||||
|
const void* buf = nullptr;
|
||||||
|
int read_bytes = mju_readResource(resource, &buf);
|
||||||
|
EXPECT_EQ(read_bytes, nbytes);
|
||||||
|
EXPECT_EQ(std::memcmp(buf, data, nbytes), 0);
|
||||||
|
|
||||||
|
mju_closeResource(resource);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
std::remove(tmpfile.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ResourceTest, WriteResourceNoWriteCallback) {
|
||||||
|
// Register a read-only provider (no write callback)
|
||||||
|
mjpResourceProvider provider = {
|
||||||
|
.prefix = "rdonly",
|
||||||
|
.open = open_nop,
|
||||||
|
.read = read_nop,
|
||||||
|
.close = close_nop,
|
||||||
|
};
|
||||||
|
|
||||||
|
mjp_registerResourceProvider(&provider);
|
||||||
|
|
||||||
|
const char* data = "data";
|
||||||
|
mjtSize written = mju_writeResource("rdonly:somefile", data, 4, nullptr, nullptr, 0);
|
||||||
|
EXPECT_EQ(written, -1);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace mujoco
|
} // namespace mujoco
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ _SKIPPED_UTILITY_FUNCTIONS: tuple[str, ...] = (
|
|||||||
"mju_isModifiedResource",
|
"mju_isModifiedResource",
|
||||||
"mju_openResource",
|
"mju_openResource",
|
||||||
"mju_readResource",
|
"mju_readResource",
|
||||||
|
"mju_writeResource",
|
||||||
# go/keep-sorted end
|
# go/keep-sorted end
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user