Commit a011d128 authored by Kevin Modzelewski's avatar Kevin Modzelewski

Save+reuse attrwrappers we generate for objects

With the goal of making `assert obj.__dict__ is obj.__dict__` work.
I haven't seen people do that, but people (gflags, doctest? others?)
definitely need `assert globals() is globals()` to pass, which uses
the same mechanism.
parent 4e6bd9db
......@@ -425,7 +425,7 @@ extern "C" PyObject* PyModule_GetDict(PyObject* _m) noexcept {
BoxedModule* m = static_cast<BoxedModule*>(_m);
assert(m->cls == module_cls);
return makeAttrWrapper(m);
return m->getAttrWrapper();
}
extern "C" int PyModule_AddObject(PyObject* _m, const char* name, PyObject* value) noexcept {
......
......@@ -2706,7 +2706,7 @@ extern "C" int PyType_Ready(PyTypeObject* cls) noexcept {
cls->giveAttr("__base__", base);
assert(cls->tp_dict == NULL);
cls->tp_dict = makeAttrWrapper(cls);
cls->tp_dict = cls->getAttrWrapper();
assert(cls->tp_name);
// tp_name
......
......@@ -516,7 +516,7 @@ Box* exec(Box* boxedCode, Box* globals, Box* locals) {
// From CPython (they set it to be f->f_builtins):
Box* globals_dict = globals;
if (globals->cls == module_cls)
globals_dict = makeAttrWrapper(globals);
globals_dict = globals->getAttrWrapper();
if (PyDict_GetItemString(globals_dict, "__builtins__") == NULL)
PyDict_SetItemString(globals_dict, "__builtins__", builtins_module);
}
......
......@@ -596,7 +596,7 @@ Box* getGlobalsDict() {
return NULL;
if (isSubclass(globals->cls, module_cls))
return makeAttrWrapper(globals);
return globals->getAttrWrapper();
return globals;
}
......@@ -735,7 +735,7 @@ Box* PythonFrameIterator::fastLocalsToBoxedLocals() {
// TODO we should cache this in frame_info->locals or something so that locals()
// (and globals() too) will always return the same dict
RELEASE_ASSERT(cf->clfunc->source->scoping->areGlobalsFromModule(), "");
return makeAttrWrapper(cf->clfunc->source->parent_module);
return cf->clfunc->source->parent_module->getAttrWrapper();
}
if (impl->getId().type == PythonFrameId::COMPILED) {
......
......@@ -430,6 +430,12 @@ class Box {
private:
BoxedDict** getDictPtr();
// Adds a new attribute to a HCAttrs-backed object. Must pass in the new hidden class object
// which must be the same as the current hidden class but with the new attribute at the end.
// Swaps the hidden class, reallocates and copies and updates the attribute array.
// The value of the current hidden class should be guarded before calling this.
void addNewHCAttr(HiddenClass* new_hcls, Box* val, SetattrRewriteArgs* rewrite_args);
public:
// Add a no-op constructor to make sure that we don't zero-initialize cls
Box() {}
......@@ -459,6 +465,9 @@ public:
bool hasattr(const std::string& attr) { return getattr(attr) != NULL; }
void delattr(const std::string& attr, DelattrRewriteArgs* rewrite_args);
// Only valid for hc-backed instances:
Box* getAttrWrapper();
Box* reprIC();
BoxedString* reprICAsString();
bool nonzeroIC();
......
......@@ -81,7 +81,7 @@ extern "C" Box* vars(Box* obj) {
if (!obj)
return fastLocalsToBoxedLocals();
return makeAttrWrapper(obj);
return obj->getAttrWrapper();
}
extern "C" Box* abs_(Box* x) {
......@@ -587,7 +587,7 @@ public:
RELEASE_ASSERT(isSubclass(self->cls, BaseException), "");
BoxedException* exc = static_cast<BoxedException*>(self);
return BoxedTuple::create({ self->cls, EmptyTuple, makeAttrWrapper(self) });
return BoxedTuple::create({ self->cls, EmptyTuple, self->getAttrWrapper() });
}
};
......
......@@ -150,7 +150,7 @@ static Box* classobjGetattribute(Box* _cls, Box* _attr) {
// These are special cases in CPython as well:
if (attr->s[0] == '_' && attr->s[1] == '_') {
if (attr->s == "__dict__")
return makeAttrWrapper(cls);
return cls->getAttrWrapper();
if (attr->s == "__bases__")
return cls->bases;
......@@ -255,7 +255,7 @@ static Box* _instanceGetattribute(Box* _inst, Box* _attr, bool raise_on_missing)
// These are special cases in CPython as well:
if (attr->s[0] == '_' && attr->s[1] == '_') {
if (attr->s == "__dict__")
return makeAttrWrapper(inst);
return inst->getAttrWrapper();
if (attr->s == "__class__")
return inst->inst_cls;
......
......@@ -125,7 +125,7 @@ Box* getFrame(int depth) {
auto cf = it.getCF();
BoxedFrame* f = fi->frame_obj = new BoxedFrame(std::move(it));
assert(cf->clfunc->source->scoping->areGlobalsFromModule());
f->_globals = makeAttrWrapper(cf->clfunc->source->parent_module);
f->_globals = cf->clfunc->source->parent_module->getAttrWrapper();
f->_code = codeForCLFunction(cf->clfunc);
}
......
......@@ -400,7 +400,7 @@ void BoxedClass::finishInitialization() {
}
assert(!this->tp_dict);
this->tp_dict = makeAttrWrapper(this);
this->tp_dict = this->getAttrWrapper();
commonClassSetup(this);
}
......@@ -490,19 +490,31 @@ HiddenClass* HiddenClass::getOrMakeChild(const std::string& attr) {
HiddenClass* rtn = new HiddenClass(this);
this->children[attr] = rtn;
rtn->attr_offsets[attr] = attr_offsets.size();
rtn->attr_offsets[attr] = this->attributeArraySize();
assert(rtn->attributeArraySize() == this->attributeArraySize() + 1);
return rtn;
}
HiddenClass* HiddenClass::getAttrwrapperChild() {
assert(type == NORMAL);
if (!attrwrapper_child) {
attrwrapper_child = new HiddenClass(this);
attrwrapper_child->attrwrapper_offset = this->attributeArraySize();
assert(attrwrapper_child->attributeArraySize() == this->attributeArraySize() + 1);
}
return attrwrapper_child;
}
/**
* del attr from current HiddenClass, pertain the orders of remaining attrs
* del attr from current HiddenClass, maintaining the order of the remaining attrs
*/
HiddenClass* HiddenClass::delAttrToMakeHC(const std::string& attr) {
assert(type == NORMAL);
int idx = getOffset(attr);
assert(idx >= 0);
std::vector<std::string> new_attrs(attr_offsets.size() - 1);
std::vector<std::string> new_attrs(attributeArraySize() - 1);
for (auto it = attr_offsets.begin(); it != attr_offsets.end(); ++it) {
if (it->second < idx)
new_attrs[it->second] = it->first();
......@@ -511,11 +523,20 @@ HiddenClass* HiddenClass::delAttrToMakeHC(const std::string& attr) {
}
}
int new_attrwrapper_offset = attrwrapper_offset;
if (new_attrwrapper_offset > idx)
new_attrwrapper_offset--;
// TODO we can first locate the parent HiddenClass of the deleted
// attribute and hence avoid creation of its ancestors.
HiddenClass* cur = root_hcls;
int curidx = 0;
for (const auto& attr : new_attrs) {
cur = cur->getOrMakeChild(attr);
if (curidx == new_attrwrapper_offset)
cur = cur->getAttrwrapperChild();
else
cur = cur->getOrMakeChild(attr);
curidx++;
}
return cur;
}
......@@ -648,6 +669,58 @@ Box* Box::getattr(const std::string& attr, GetattrRewriteArgs* rewrite_args) {
return NULL;
}
void Box::addNewHCAttr(HiddenClass* new_hcls, Box* new_attr, SetattrRewriteArgs* rewrite_args) {
assert(cls->instancesHaveHCAttrs());
HCAttrs* attrs = getHCAttrsPtr();
HiddenClass* hcls = attrs->hcls;
#ifndef NDEBUG
// make sure we don't need to rearrange the attributes
assert(new_hcls->attributeArraySize() == hcls->attributeArraySize() + 1);
for (const auto& p : hcls->getStrAttrOffsets()) {
assert(new_hcls->getStrAttrOffsets().lookup(p.first()) == p.second);
}
if (hcls->getAttrwrapperOffset() != -1)
assert(hcls->getAttrwrapperOffset() == new_hcls->getAttrwrapperOffset());
#endif
int numattrs = hcls->attributeArraySize();
RewriterVar* r_new_array2 = NULL;
int new_size = sizeof(HCAttrs::AttrList) + sizeof(Box*) * (numattrs + 1);
if (numattrs == 0) {
attrs->attr_list = (HCAttrs::AttrList*)gc_alloc(new_size, gc::GCKind::PRECISE);
if (rewrite_args) {
RewriterVar* r_newsize = rewrite_args->rewriter->loadConst(new_size, Location::forArg(0));
RewriterVar* r_kind = rewrite_args->rewriter->loadConst((int)gc::GCKind::PRECISE, Location::forArg(1));
r_new_array2 = rewrite_args->rewriter->call(true, (void*)gc::gc_alloc, r_newsize, r_kind);
}
} else {
attrs->attr_list = (HCAttrs::AttrList*)gc::gc_realloc(attrs->attr_list, new_size);
if (rewrite_args) {
RewriterVar* r_oldarray
= rewrite_args->obj->getAttr(cls->attrs_offset + HCATTRS_ATTRS_OFFSET, Location::forArg(0));
RewriterVar* r_newsize = rewrite_args->rewriter->loadConst(new_size, Location::forArg(1));
r_new_array2 = rewrite_args->rewriter->call(true, (void*)gc::gc_realloc, r_oldarray, r_newsize);
}
}
// Don't set the new hcls until after we do the allocation for the new attr_list;
// that allocation can cause a collection, and we want the collector to always
// see a consistent state between the hcls and the attr_list
attrs->hcls = new_hcls;
if (rewrite_args) {
r_new_array2->setAttr(numattrs * sizeof(Box*) + ATTRLIST_ATTRS_OFFSET, rewrite_args->attrval);
rewrite_args->obj->setAttr(cls->attrs_offset + HCATTRS_ATTRS_OFFSET, r_new_array2);
RewriterVar* r_hcls = rewrite_args->rewriter->loadConst((intptr_t)new_hcls);
rewrite_args->obj->setAttr(cls->attrs_offset + HCATTRS_HCLS_OFFSET, r_hcls);
rewrite_args->out_success = true;
}
attrs->attr_list->attrs[numattrs] = new_attr;
}
void Box::setattr(const std::string& attr, Box* val, SetattrRewriteArgs* rewrite_args) {
assert(gc::isValidGCObject(val));
......@@ -681,7 +754,6 @@ void Box::setattr(const std::string& attr, Box* val, SetattrRewriteArgs* rewrite
}
assert(hcls->type == HiddenClass::NORMAL);
int numattrs = hcls->getAttrOffsets().size();
int offset = hcls->getOffset(attr);
......@@ -691,7 +763,7 @@ void Box::setattr(const std::string& attr, Box* val, SetattrRewriteArgs* rewrite
}
if (offset >= 0) {
assert(offset < numattrs);
assert(offset < hcls->attributeArraySize());
Box* prev = attrs->attr_list->attrs[offset];
attrs->attr_list->attrs[offset] = val;
......@@ -711,47 +783,11 @@ void Box::setattr(const std::string& attr, Box* val, SetattrRewriteArgs* rewrite
assert(offset == -1);
HiddenClass* new_hcls = hcls->getOrMakeChild(attr);
// TODO need to make sure we don't need to rearrange the attributes
assert(new_hcls->getAttrOffsets().lookup(attr) == numattrs);
#ifndef NDEBUG
for (const auto& p : hcls->getAttrOffsets()) {
assert(new_hcls->getAttrOffsets().lookup(p.first()) == p.second);
}
#endif
RewriterVar* r_new_array2 = NULL;
int new_size = sizeof(HCAttrs::AttrList) + sizeof(Box*) * (numattrs + 1);
if (numattrs == 0) {
attrs->attr_list = (HCAttrs::AttrList*)gc_alloc(new_size, gc::GCKind::PRECISE);
if (rewrite_args) {
RewriterVar* r_newsize = rewrite_args->rewriter->loadConst(new_size, Location::forArg(0));
RewriterVar* r_kind = rewrite_args->rewriter->loadConst((int)gc::GCKind::PRECISE, Location::forArg(1));
r_new_array2 = rewrite_args->rewriter->call(true, (void*)gc::gc_alloc, r_newsize, r_kind);
}
} else {
attrs->attr_list = (HCAttrs::AttrList*)gc::gc_realloc(attrs->attr_list, new_size);
if (rewrite_args) {
RewriterVar* r_oldarray
= rewrite_args->obj->getAttr(cls->attrs_offset + HCATTRS_ATTRS_OFFSET, Location::forArg(0));
RewriterVar* r_newsize = rewrite_args->rewriter->loadConst(new_size, Location::forArg(1));
r_new_array2 = rewrite_args->rewriter->call(true, (void*)gc::gc_realloc, r_oldarray, r_newsize);
}
}
// Don't set the new hcls until after we do the allocation for the new attr_list;
// that allocation can cause a collection, and we want the collector to always
// see a consistent state between the hcls and the attr_list
attrs->hcls = new_hcls;
if (rewrite_args) {
r_new_array2->setAttr(numattrs * sizeof(Box*) + ATTRLIST_ATTRS_OFFSET, rewrite_args->attrval);
rewrite_args->obj->setAttr(cls->attrs_offset + HCATTRS_ATTRS_OFFSET, r_new_array2);
// make sure we don't need to rearrange the attributes
assert(new_hcls->getStrAttrOffsets().lookup(attr) == hcls->attributeArraySize());
RewriterVar* r_hcls = rewrite_args->rewriter->loadConst((intptr_t)new_hcls);
rewrite_args->obj->setAttr(cls->attrs_offset + HCATTRS_HCLS_OFFSET, r_hcls);
addNewHCAttr(new_hcls, val, rewrite_args);
rewrite_args->out_success = true;
}
attrs->attr_list->attrs[numattrs] = val;
return;
}
......@@ -3727,7 +3763,7 @@ void Box::delattr(const std::string& attr, DelattrRewriteArgs* rewrite_args) {
// The order of attributes is pertained as delAttrToMakeHC constructs
// the new HiddenClass by invoking getOrMakeChild in the prevous order
// of remaining attributes
int num_attrs = hcls->getAttrOffsets().size();
int num_attrs = hcls->attributeArraySize();
int offset = hcls->getOffset(attr);
assert(offset >= 0);
Box** start = attrs->attr_list->attrs;
......@@ -4499,7 +4535,7 @@ extern "C" Box* importStar(Box* _from_module, BoxedModule* to_module) {
}
HCAttrs* module_attrs = from_module->getHCAttrsPtr();
for (auto& p : module_attrs->hcls->getAttrOffsets()) {
for (auto& p : module_attrs->hcls->getStrAttrOffsets()) {
if (p.first()[0] == '_')
continue;
......
......@@ -157,10 +157,11 @@ void _printStacktrace() {
extern "C" void abort() {
static void (*libc_abort)() = (void (*)())dlsym(RTLD_NEXT, "abort");
// In case something calls abort down the line:
// In case displaying the traceback recursively calls abort:
static bool recursive = false;
// If object_cls is NULL, then we somehow died early on, and won't be able to display a traceback.
if (!recursive && object_cls) {
// If traceback_cls is NULL, then we somehow died early on, and won't be able to display a traceback.
if (!recursive && traceback_cls) {
recursive = true;
fprintf(stderr, "Someone called abort!\n");
......@@ -178,7 +179,7 @@ extern "C" void abort() {
}
// Cancel the alarm.
// This is helpful for when running in a debugger, since the debugger will catch the
// This is helpful for when running in a debugger, since otherwise the debugger will catch the
// abort and let you investigate, but the alarm will still come back to kill the program.
alarm(0);
}
......
......@@ -477,7 +477,7 @@ extern "C" void typeGCHandler(GCVisitor* v, Box* b) {
static Box* typeDict(Box* obj, void* context) {
if (obj->cls->instancesHaveHCAttrs())
return makeAttrWrapper(obj);
return obj->getAttrWrapper();
if (obj->cls->instancesHaveDictAttrs())
return obj->getDict();
abort();
......@@ -1311,7 +1311,7 @@ public:
HCAttrs* attrs = self->b->getHCAttrsPtr();
RELEASE_ASSERT(attrs->hcls->type == HiddenClass::NORMAL, "");
bool first = true;
for (const auto& p : attrs->hcls->getAttrOffsets()) {
for (const auto& p : attrs->hcls->getStrAttrOffsets()) {
if (!first)
os << ", ";
first = false;
......@@ -1343,7 +1343,7 @@ public:
HCAttrs* attrs = self->b->getHCAttrsPtr();
RELEASE_ASSERT(attrs->hcls->type == HiddenClass::NORMAL, "");
for (const auto& p : attrs->hcls->getAttrOffsets()) {
for (const auto& p : attrs->hcls->getStrAttrOffsets()) {
listAppend(rtn, boxString(p.first()));
}
return rtn;
......@@ -1357,7 +1357,7 @@ public:
HCAttrs* attrs = self->b->getHCAttrsPtr();
RELEASE_ASSERT(attrs->hcls->type == HiddenClass::NORMAL, "");
for (const auto& p : attrs->hcls->getAttrOffsets()) {
for (const auto& p : attrs->hcls->getStrAttrOffsets()) {
listAppend(rtn, attrs->attr_list->attrs[p.second]);
}
return rtn;
......@@ -1371,7 +1371,7 @@ public:
HCAttrs* attrs = self->b->getHCAttrsPtr();
RELEASE_ASSERT(attrs->hcls->type == HiddenClass::NORMAL, "");
for (const auto& p : attrs->hcls->getAttrOffsets()) {
for (const auto& p : attrs->hcls->getStrAttrOffsets()) {
BoxedTuple* t = BoxedTuple::create({ boxString(p.first()), attrs->attr_list->attrs[p.second] });
listAppend(rtn, t);
}
......@@ -1386,7 +1386,7 @@ public:
HCAttrs* attrs = self->b->getHCAttrsPtr();
RELEASE_ASSERT(attrs->hcls->type == HiddenClass::NORMAL, "");
for (const auto& p : attrs->hcls->getAttrOffsets()) {
for (const auto& p : attrs->hcls->getStrAttrOffsets()) {
rtn->d[boxString(p.first())] = attrs->attr_list->attrs[p.second];
}
return rtn;
......@@ -1398,7 +1398,7 @@ public:
HCAttrs* attrs = self->b->getHCAttrsPtr();
RELEASE_ASSERT(attrs->hcls->type == HiddenClass::NORMAL, "");
return boxInt(attrs->hcls->getAttrOffsets().size());
return boxInt(attrs->hcls->getStrAttrOffsets().size());
}
static Box* update(Box* _self, Box* _container) {
......@@ -1410,7 +1410,7 @@ public:
HCAttrs* attrs = container->b->getHCAttrsPtr();
RELEASE_ASSERT(attrs->hcls->type == HiddenClass::NORMAL, "");
for (const auto& p : attrs->hcls->getAttrOffsets()) {
for (const auto& p : attrs->hcls->getStrAttrOffsets()) {
self->b->setattr(p.first(), attrs->attr_list->attrs[p.second], NULL);
}
} else if (_container->cls == dict_cls) {
......@@ -1439,7 +1439,7 @@ AttrWrapperIter::AttrWrapperIter(AttrWrapper* aw) {
hcls = aw->b->getHCAttrsPtr()->hcls;
assert(hcls);
RELEASE_ASSERT(hcls->type == HiddenClass::NORMAL, "");
it = hcls->getAttrOffsets().begin();
it = hcls->getStrAttrOffsets().begin();
}
Box* AttrWrapperIter::hasnext(Box* _self) {
......@@ -1447,7 +1447,7 @@ Box* AttrWrapperIter::hasnext(Box* _self) {
AttrWrapperIter* self = static_cast<AttrWrapperIter*>(_self);
RELEASE_ASSERT(self->hcls->type == HiddenClass::NORMAL, "");
return boxBool(self->it != self->hcls->getAttrOffsets().end());
return boxBool(self->it != self->hcls->getStrAttrOffsets().end());
}
Box* AttrWrapperIter::next(Box* _self) {
......@@ -1455,19 +1455,28 @@ Box* AttrWrapperIter::next(Box* _self) {
AttrWrapperIter* self = static_cast<AttrWrapperIter*>(_self);
RELEASE_ASSERT(self->hcls->type == HiddenClass::NORMAL, "");
assert(self->it != self->hcls->getAttrOffsets().end());
assert(self->it != self->hcls->getStrAttrOffsets().end());
Box* r = boxString(self->it->first());
++self->it;
return r;
}
Box* makeAttrWrapper(Box* b) {
assert(b->cls->instancesHaveHCAttrs());
if (b->getHCAttrsPtr()->hcls->type == HiddenClass::DICT_BACKED) {
return b->getHCAttrsPtr()->attr_list->attrs[0];
Box* Box::getAttrWrapper() {
assert(cls->instancesHaveHCAttrs());
HCAttrs* attrs = getHCAttrsPtr();
HiddenClass* hcls = attrs->hcls;
if (hcls->type == HiddenClass::DICT_BACKED) {
return attrs->attr_list->attrs[0];
}
return new AttrWrapper(b);
int offset = hcls->getAttrwrapperOffset();
if (offset == -1) {
Box* aw = new AttrWrapper(this);
addNewHCAttr(hcls->getAttrwrapperChild(), aw, NULL);
return aw;
}
return attrs->attr_list->attrs[offset];
}
Box* unwrapAttrWrapper(Box* b) {
......
......@@ -283,16 +283,19 @@ public:
private:
HiddenClass(HCType type) : type(type) {}
HiddenClass(HiddenClass* parent) : type(NORMAL), attr_offsets() {
HiddenClass(HiddenClass* parent) : type(NORMAL), attr_offsets(), attrwrapper_offset(parent->attrwrapper_offset) {
assert(parent->type == NORMAL);
for (auto& p : parent->attr_offsets) {
this->attr_offsets.insert(&p);
}
}
// Only makes sense for NORMAL hidden classes. Clients should access through getAttrOffsets():
// These fields only make sense for NORMAL hidden classes:
llvm::StringMap<int> attr_offsets;
ContiguousMap<llvm::StringRef, HiddenClass*, llvm::StringMap<int>> children;
// If >= 0, is the offset where we stored an attrwrapper object
int attrwrapper_offset = -1;
HiddenClass* attrwrapper_child = NULL;
public:
static HiddenClass* makeRoot() {
......@@ -315,18 +318,35 @@ public:
void gc_visit(GCVisitor* visitor) {
// Visit children even for the dict-backed case, since children will just be empty
visitor->visitRange((void* const*)&children.vector()[0], (void* const*)&children.vector()[children.size()]);
if (attrwrapper_child)
visitor->visit(attrwrapper_child);
}
// Only makes sense for NORMAL hidden classes:
const llvm::StringMap<int>& getAttrOffsets() {
// The total size of the attribute array. The slots in the attribute array may not correspond 1:1 to Python
// attributes.
int attributeArraySize() {
if (type == DICT_BACKED)
return 1;
ASSERT(type == NORMAL, "%d", type);
int r = attr_offsets.size();
if (attrwrapper_offset != -1)
r += 1;
return r;
}
// The mapping from string attribute names to attribute offsets. There may be other objects in the attributes
// array.
// Only valid for NORMAL hidden classes
const llvm::StringMap<int>& getStrAttrOffsets() {
assert(type == NORMAL);
return attr_offsets;
}
// Only makes sense for NORMAL hidden classes:
// Only valid for NORMAL hidden classes:
HiddenClass* getOrMakeChild(const std::string& attr);
// Only makes sense for NORMAL hidden classes:
// Only valid for NORMAL hidden classes:
int getOffset(const std::string& attr) {
assert(type == NORMAL);
auto it = attr_offsets.find(attr);
......@@ -335,7 +355,14 @@ public:
return it->second;
}
// Only makes sense for NORMAL hidden classes:
int getAttrwrapperOffset() {
assert(type == NORMAL);
return attrwrapper_offset;
}
HiddenClass* getAttrwrapperChild();
// Only valid for NORMAL hidden classes:
HiddenClass* delAttrToMakeHC(const std::string& attr);
};
......@@ -803,7 +830,6 @@ extern "C" void boxGCHandler(GCVisitor* v, Box* b);
Box* objectNewNoArgs(BoxedClass* cls);
Box* objectSetattr(Box* obj, Box* attr, Box* value);
Box* makeAttrWrapper(Box* b);
Box* unwrapAttrWrapper(Box* b);
Box* attrwrapperKeys(Box* b);
void attrwrapperDel(Box* b, const std::string& attr);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment