The following describes a couple of APIs, along with a contract, for portable binary data in Solar. These are a refinement of some time-tested policies used for data interoperability among various plugins. // // // // These are what you will use when PROVIDING memory, to achieve that: union MemoryWorkVar { void * p; // does not take ownership size_t size; lua_Number n; unsigned int u; int i; } struct MemoryWorkspace { MemoryWorkVar vars[8]; // scratch memory, suitable for garden-variety callbacks char error[64]; // available for error reporting; `error[0]` is set to `\0` before `getObject()` is called void * data; size_t dataSize; }; struct MemoryCallbacks { const unsigned char * (*getReadableBytes)(MemoryWorkspace * ws, const MemoryWorkVar * context); // may be `NULL`, cf. `MemoryCreateInterface()` // returns the (read-only) memory corresponding to `object` // if it returns `NULL`, say if the object is temporarily invalid or empty, `getByteCount()` MUST return 0 // conversely, if `getByteCount()` returns non-0, it MUST point to memory at least satisfying that amount // `context` may be used to provide input to the getter, cf. `getWriteableBytes()` // it must be valid or at least safe to provide `NULL` unsigned char * (*getWriteableBytes)(MemoryWorkspace * ws, const MemoryWorkVar * context); // as per `getReadableBytes()`, but returns writeable memory // an example of `context` might be as a key: // while the memory might indeed be writeable, only some callers should be "trusted" with it // passing in a valid key indicates the caller has this trust // as with `getReadableBytes()`, `context` may be `NULL` and this must be valid or at least safe size_t (*getByteCount)(MemoryWorkspace * ws); // required // returns the number of bytes that may be read from / written to `object`'s memory int (*resize)(MemoryWorkspace * ws, size_t size, const MemoryWorkVar * context); // may be `NULL`; // returning non-0 indicates success // the idea behind `context` follows `getReadableBytes()` and `getWriteableBytes()` // if the size did change, a new call to `getByteCount()` must reflect that // `getByteCount()` must also gracefully account for a failed resize (returning 0, say) // These are fairly specialized operations, and may each be `NULL`: while some special-purpose logic might // always check them just to be thorough, it would be too much trouble accounting for these properties on // each and every `MemoryAcquireInterface()` call: as a "good citizen", a memory provider should advertise // (in the docs, say) when one or more are valid, so any consumer knows to bother consulting them. size_t (*getAlignment)(MemoryWorkspace * ws); // the result should be 0 (a "normal" allocation) or a suitable power-of-2 int (*getDimension)(MemoryWorkspace * ws, unsigned int index, size_t * dim); // returning 0 indicates `index` >= #dims; otherwise `dim` must be populated int (*getStride)(MemoryWorkspace * ws, unsigned int index, size_t * stride); // as with `getDimension()`, mutatis mutandis }; struct MemoryInterfaceInfo { MemoryCallbacks callbacks; int (*getObject)(lua_State * L, int arg, MemoryWorkspace * ws); // required // used by `MemoryAcquireInterface()` to prepare the callbacks' workspace // returning non-0 indicates success // `arg` will be normalized // cf. the details about the light userdata policy, below // once this call begins, `ws` is never modified internally // similarly, the Lua object is no longer used internally // any changes to the Lua stack will remain after the call int dataSize; // if == 0 `data` (in `MemoryWorkspace`) will be `NULL`, `dataSize` 0 // > 0 // the memory interface proxy will be created with `lua_newuserdata(L, dataSize)` // the userdata's block address will be supplied as `data`; `dataSize` is carried over // < 0 // a userdata must be on top of the stack (light or full); it will be added to the proxy's environment // `lua_touserdata()` will be supplied as `data`, and `lua_objlen()` as `dataSize` (n.b. for light userdata, this will be 0) }; // // // int MemoryCreateInterface (lua_State * L, const MemoryInterfaceInfo * mii); // returns non-0 on success, and pushes a memory interface proxy onto the stack // at a minimum, the interface must implement `getByteCount()`, plus either `getReadableBytes()` or `getWriteableBytes()` // if `dataSize` was > 0, do a `lua_touserdata()` on the proxy to retrieve and populate `data` // to use the interface, assign the proxy to a metatable's `__memory` field // the proxy will be assigned a memory API version based on the Solar library being linked against // the proxy is given a unique environment // integer keys are used internally // others are fine for custom use // // // Basic example, a read-only userdata: lua_newuserdata(L, sizeof(MyUserData)); if (luaL_newmetatable(L, "MyUserdataType")) { MemoryInterfaceInfo mii = { 0 }; mii.getObject = [](lua_State * L, int arg, MemoryWorkspace * ws) { ws->vars[0].p = lua_checkudata(L, arg, "MyUserdataType"); ws->vars[1].size = lua_objlen(L, arg); return 1; }; mii.callbacks.getReadableBytes = [](MemoryWorkspace * ws, const MemoryWorkVar *) { return static_cast(ws->vars[0].p); }; mii.callbacks.getByteCount = [](MemoryWorkspace * ws) { return ws->vars[1].size; }; MemoryCreateInterface(L, &mii); lua_setfield(L, -1, "__memory"); // methods, etc... } lua_setmetatable(L, -2); // // // A resizable object, with write privileges: struct MyObject { std::vector childObjects; // other stuff static const void * GetKey () // give this to objects that may do writes { static int sKey; return &sKey; } } MyObject * object = lua_newuserdata(L, sizeof(MyObject)); new (object) MyObject; if (luaL_newmetatable(L, "MyObjectType")) { MemoryInterfaceInfo mii = { 0 }; mii.getObject = [](lua_State * L, int arg, MemoryWorkspace * ws) { workspace[0].p = reinterpret_cast(lua_checkudata(L, arg, "MyObjectType")); return 1; }; mii.callbacks.getReadableBytes = [](MemoryWorkspace * ws, const MemoryWorkVar *) { return static_cast(ws->vars[0].p)->childObjects.data(); }; mii.callbacks.getWriteableBytes = [](MemoryWorkspace * ws, const MemoryWorkVar * key) { if (!key || key->p != MyObject::GetKey()) return NULL; return reinterpret_cast(ws->vars[0].p)->childObjects.data(); }; mii.callbacks.getByteCount = [](MemoryWorkspace * ws) { return reinterpret_cast(ws->vars[0].p)->childObjects.size(); }; mii.callbacks.resize = [](MemoryWorkspace * ws, unsigned int size, const MemoryWorkVar * key) { if (!key || key->p != MyObject::GetKey()) return NULL; if (size % sizeof(SubObjectType) != 0) return NULL; reinterpret_cast(ws->vars[0].p)->childObjects.resize(size); // could check for exceptions, too return 1; }; MemoryCreateInterface(L, &mii); lua_setfield(L, -1, "__memory"); // __gc, methods, etc... } lua_setmetatable(L, -2); // // // This is what you will use if you are CONSUMING memory: struct MemoryCallbacksInfo; // forward reference // uses Pimpl idiom to handle versioning // these are built upon whatever callbacks were provided // always-fail / no-op dummies are supplied where a callback is absent (not provided, or unavailable in the corresponding memory API version) struct MemoryInterface { // version 0+: const unsigned char * (*getReadableBytes)(MemoryCallbacksInfo * mci, const MemoryWorkVar * context); const unsigned char * (*getReadableBytesOfSize)(MemoryCallbacksInfo * mci, size_t n, const MemoryWorkVar * context); // possible `resize()`, then `getReadableBytes()` void (*copyBytesTo)(MemoryCallbacksInfo * mci, unsigned char * output, size_t outputSize, int ignoreExtra, const MemoryWorkVar * context); // `getReadableBytes()`, copy of `min(getByteCount(), outputSize)` to `output` // if `outputSize` > `getByteCount()` // the rest is set to 0, unless `ignoreExtra` is non-0 unsigned char * (*getWriteableBytes)(MemoryCallbacksInfo * mci, const MemoryWorkVar * context); unsigned char * (*getWriteableBytesOfSize)(MemoryCallbacksInfo * mci, size_t n, const MemoryWorkVar * context); // possible `resize()`, then `getWriteableBytes()` int (*resize)(MemoryCallbacksInfo * mci, unsigned int size, const MemoryWorkVar * context); size_t (*getByteCount)(MemoryCallbacksInfo * mci); size_t (*getAlignment)(MemoryCallbacksInfo * mci); int (*getDimension)(MemoryCallbacksInfo * mci, unsigned int index, size_t * dim); int (*getStride)(MemoryCallbacksInfo * mci, unsigned int index, size_t * stride); }; struct MemoryCallbacksInfo { MemoryInterface interface; // these build on `callbacks` and provide the "proper" way to call them // implementation details, cf. `MemoryInterfaceInfo` void * object; const MemoryCallbacks * callbacks; MemoryWorkspace workspace; int version; // version of memory API, as seen by the `MemoryCreateInterface()` call that supplied the object's interface // it describes the memory feature set known to the object // this is currently always 0 (original feature set), but would be incremented if and when e.g. new APIs are added (docs would list what changed and when, version-wise) // the upshot being this would diverge if two libraries were compiled against different feature levels }; int MemoryAcquireInterface (lua_State * L, int arg, MemoryCallbacksInfo * info); // non-0 on success; on failure, `interface` will be fully dummied // // // Roughly, `MemoryAcquireInterface()` would be implemented something like this: if LUA_TSTRING == type get built-in string interface (callbacks have `getReadableBytes()` and `getByteCount()`) elseif luaL_getmetafield(L, arg, "__memory") && HAS_METATABLE(L, -1, "MemoryInterface") if `lua_objlen()` of proxy > 0 use those bytes else get userdata from environment, point to it get callbacks and version from environment end supply appropriate wrappers for callbacks // // // So, in Lua you might have some function and call it in a few ways: MyFunction("ksdfjsdfks", 22, 31) MyFunction(myUserData, 13, 7) -- an instance of the `MyUserData` type Inside the `MyFunction` implementation, you'd have some code like: MemoryCallbacksInfo mci; if (MemoryAcquireInterface(L, 1, &mci) && mci.interface.getByteCount(&mci) > 0) // will be appropriate interface: string -> bytes, or MyUserData -> bytes { UseBytes(mci.interface.getReadableBytes(&mci), mci.interface.getByteCount(&mci)); } // // // Possible macro: #define CORONA_IFC(NAME, OBJECT, ...) OBJECT.interface.NAME(&OBJECT, __VA_ARGS__) Then do: MemoryCallbacksInfo mci; if (MemoryAcquireInterface(L, 1, &mci) && CORONA_IFC(mci, getByteCount) > 0) // will be appropriate interface: string -> bytes, or MyUserData -> bytes { UseBytes(CORONA_IFC(mci, getReadableBytes), CORONA_IFC(mci, getByteCount)); } // // // This is a rough policy for light userdata. It would be suited for, say, accessing members of a large structure or array without introducing a lot of temporaries. The userdata would not be raw addresses, as is their wont, but rather bundle some information: reserve <= 16 bits for an ID; maybe a few bits should contain a fixed pattern as a validity check look up the ID in some table in the registry from here on, this behaves like `MemoryAcquireInterface()`, except the first few work vars are given initial values: ws->vars[0].u = 1 (will be 0 in a "normal" acquire) ws->vars[1].u = ID ws->vars[2].u = context (the remaining bits aside from the ID and such; probably 10-12 bits on a 32-bit target (actually, maybe a full 16 would be good and impose the limit on IDs) This suggests a trio of APIs: int MemoryBindLookupSlot (int arg, uint16 * id); // returns non-0 on success, and populates `id` // per the above, the `id` would reference a memory proxy that was on top of the stack, adding it to the registry table void MemoryReleaseLookupSlot (uint16 id); // this would be called, say, in an object's `__gc`, a "finalize" event, or some other `destroy()` logic // `id` may again be allocated after this; any lingering encodings are invalidated int MemoryPushLookupEncoding (uint16 id, uint16 context); // returns non-0 on success, and pushes the encoded pair onto the stack