hphp/runtime/base/object-data.cpp (1,350 lines of code) (raw):
/*
+----------------------------------------------------------------------+
| HipHop for PHP |
+----------------------------------------------------------------------+
| Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com) |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| http://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
*/
#include "hphp/runtime/base/object-data.h"
#include "hphp/runtime/base/array-init.h"
#include "hphp/runtime/base/builtin-functions.h"
#include "hphp/runtime/base/collections.h"
#include "hphp/runtime/base/container-functions.h"
#include "hphp/runtime/base/datatype.h"
#include "hphp/runtime/base/exceptions.h"
#include "hphp/runtime/base/execution-context.h"
#include "hphp/runtime/base/object-iterator.h"
#include "hphp/runtime/base/request-info.h"
#include "hphp/runtime/base/runtime-error.h"
#include "hphp/runtime/base/tv-comparisons.h"
#include "hphp/runtime/base/tv-refcount.h"
#include "hphp/runtime/base/tv-type.h"
#include "hphp/runtime/base/type-variant.h"
#include "hphp/runtime/base/vanilla-dict-defs.h"
#include "hphp/runtime/base/variable-serializer.h"
#include "hphp/runtime/ext/generator/ext_generator.h"
#include "hphp/runtime/ext/simplexml/ext_simplexml.h"
#include "hphp/runtime/ext/datetime/ext_datetime.h"
#include "hphp/runtime/ext/std/ext_std_closure.h"
#include "hphp/runtime/vm/class.h"
#include "hphp/runtime/vm/member-operations.h"
#include "hphp/runtime/vm/memo-cache.h"
#include "hphp/runtime/vm/native-data.h"
#include "hphp/runtime/vm/native-prop-handler.h"
#include "hphp/runtime/vm/jit/translator-inline.h"
#include "hphp/runtime/vm/repo-global-data.h"
#include "hphp/system/systemlib.h"
#include <folly/Hash.h>
#include <folly/ScopeGuard.h>
#include <vector>
namespace HPHP {
//////////////////////////////////////////////////////////////////////
TRACE_SET_MOD(runtime);
//////////////////////////////////////////////////////////////////////
namespace {
const StaticString s_clone("__clone");
ALWAYS_INLINE
void verifyTypeHint(const Class* thisCls,
const Class::Prop* prop,
tv_lval val) {
assertx(tvIsPlausible(*val));
assertx(type(val) != KindOfUninit);
if (!prop || RuntimeOption::EvalCheckPropTypeHints <= 0) return;
if (prop->typeConstraint.isCheckable()) {
prop->typeConstraint.verifyProperty(val, thisCls, prop->cls, prop->name);
}
if (RuntimeOption::EvalEnforceGenericsUB <= 0) return;
for (auto const& ub : prop->ubs) {
if (ub.isCheckable()) {
ub.verifyProperty(val, thisCls, prop->cls, prop->name);
}
}
}
ALWAYS_INLINE
void unsetTypeHint(const Class::Prop* prop) {
if (RuntimeOption::EvalCheckPropTypeHints <= 0) return;
if (!prop || prop->typeConstraint.isMixedResolved()) return;
raise_property_typehint_unset_error(
prop->cls,
prop->name,
prop->typeConstraint.isSoft(),
prop->typeConstraint.isUpperBound()
);
}
}
//////////////////////////////////////////////////////////////////////
// Check that the given property's type matches its type-hint.
namespace {
bool assertATypeHint(const TypeConstraint& tc, tv_rval val) {
if (!tc.isCheckable() || tc.isSoft()) return true;
if (val.type() == KindOfUninit) return tc.maybeMixed();
return tc.assertCheck(val);
}
}
bool ObjectData::assertTypeHint(tv_rval prop, Slot slot) const {
assertx(tvIsPlausible(*prop));
assertx(slot < m_cls->numDeclProperties());
auto const& propDecl = m_cls->declProperties()[slot];
if (prop.type() == KindOfResource && g_context->doingInlineInterp()) {
return true;
}
if (debug && RuntimeOption::RepoAuthoritative) {
// The fact that uninitialized LateInit props are uninint isn't
// reflected in the repo-auth-type.
if (prop.type() != KindOfUninit || !(propDecl.attrs & AttrLateInit)) {
always_assert(tvMatchesRepoAuthType(*prop, propDecl.repoAuthType));
}
}
// If we're not hard enforcing, then the prop might contain anything.
if (RuntimeOption::EvalCheckPropTypeHints <= 2) return true;
if (!propDecl.typeConstraint.isCheckable() ||
propDecl.typeConstraint.isSoft()) return true;
if (propDecl.typeConstraint.isUpperBound() &&
RuntimeOption::EvalEnforceGenericsUB < 2) return true;
if (prop.type() == KindOfNull && !(propDecl.attrs & AttrNoImplicitNullable)) {
return true;
}
if (prop.type() == KindOfUninit && (propDecl.attrs & AttrLateInit)) {
return true;
}
if (!assertATypeHint(propDecl.typeConstraint, prop)) return false;
if (RuntimeOption::EvalEnforceGenericsUB <= 2) return true;
for (auto const& ub : propDecl.ubs) {
if (!assertATypeHint(ub, prop)) return false;
}
return true;
}
//////////////////////////////////////////////////////////////////////
NEVER_INLINE
static void freeDynPropArray(ObjectData* inst) {
auto& table = g_context->dynPropTable;
auto it = table.find(inst);
assertx(it != end(table));
assertx(it->second.arr().isDict());
it->second.destroy();
table.erase(it);
}
NEVER_INLINE
void ObjectData::slowDestroyCases() {
assertx(slowDestroyCheck());
if (getAttribute(UsedMemoCache)) {
assertx(m_cls->hasMemoSlots());
auto const nSlots = m_cls->numMemoSlots();
for (Slot i = 0; i < nSlots; ++i) {
auto slot = memoSlot(i);
if (slot->isCache()) {
if (auto cache = slot->getCache()) req::destroy_raw(cache);
} else {
tvDecRefGen(*slot->getValue());
}
}
}
if (UNLIKELY(getAttribute(HasDynPropArr))) freeDynPropArray(this);
if (UNLIKELY(getAttribute(IsWeakRefed))) {
WeakRefData::invalidateWeakRef((uintptr_t)this);
}
auto const memoSize = m_cls->memoSize();
auto const ptr = reinterpret_cast<char*>(this) - memoSize;
tl_heap->objFreeIndex(ptr, m_cls->sizeIdx());
}
// Single check for a couple different unlikely actions during destruction.
inline bool ObjectData::slowDestroyCheck() const {
return m_aux16 & (HasDynPropArr | IsWeakRefed | UsedMemoCache | BigAllocSize);
}
void ObjectData::release(ObjectData* obj, const Class* cls) noexcept {
assertx(obj->kindIsValid());
assertx(!obj->hasInstanceDtor());
assertx(!obj->hasNativeData());
assertx(obj->getVMClass() == cls);
assertx(cls->releaseFunc() == &ObjectData::release);
assertx(obj->props()->checkInvariants(cls->numDeclProperties()));
// Note: cleanups done in this function are only run for classes without an
// instanceDtor. Some of these cleanups are duplicated in ~ObjectData, and
// your instanceDtor may call that to have them run; if you choose not to run
// ~ObjectData from your instanceDtor you MUST do some of them manually
// (e.g. invalidate WeakRefs). Some cleanups (e.g. clearing memo caches) are
// not done from ~ObjectData because it is assumed they're not needed for
// builtin classes (and in the case of memo caches, since the clearing needs
// to be done differently when there is native data).
// Finally, cleanups such as invalidating WeakRefs that have to be done for
// correctness MUST also be done in Collector::sweep, since none of the code
// in this function or the instanceDtor will be run when the object is
// collected by GC.
// `obj' is being torn down now---be careful about where/how you dereference
// it from here on.
obj->props()->release(cls->countablePropsEnd());
if (UNLIKELY(obj->slowDestroyCheck())) {
obj->slowDestroyCases();
} else {
assertx((obj->m_aux16 & BigAllocSize) == 0);
auto const memoSize = cls->memoSize();
auto const ptr = reinterpret_cast<char*>(obj) - memoSize;
assertx(memoSize == 0 ||
reinterpret_cast<const MemoNode*>(ptr)->objOff() == memoSize);
tl_heap->freeSmallIndex(ptr, cls->sizeIdx());
}
AARCH64_WALKABLE_FRAME();
}
///////////////////////////////////////////////////////////////////////////////
// class info
StrNR ObjectData::getClassName() const {
return m_cls->preClass()->nameStr();
}
bool ObjectData::instanceof(const String& s) const {
assertx(kindIsValid());
auto const cls = Class::lookup(s.get());
return cls && instanceof(cls);
}
bool ObjectData::toBooleanImpl() const noexcept {
// Note: if you add more cases here, hhbbc/class-util.cpp also needs
// to be changed.
if (isCollection()) {
if (RuntimeOption::EvalNoticeOnCollectionToBool) {
raise_notice(
"%s to boolean cast",
collections::typeToString((CollectionType)m_kind)->data()
);
}
return collections::toBool(this);
}
if (instanceof(SimpleXMLElement_classof())) {
// SimpleXMLElement is the only non-collection class that has custom bool
// casting.
if (RuntimeOption::EvalNoticeOnSimpleXMLBehavior) {
raise_notice("SimpleXMLElement to boolean cast");
}
return SimpleXMLElement_objectCast(this, KindOfBoolean).toBoolean();
}
always_assert(false);
return false;
}
int64_t ObjectData::toInt64() const {
/* SimpleXMLElement is the only class that has proper custom num casting. */
if (LIKELY(!instanceof(SimpleXMLElement_classof()))) {
throwObjToIntException(classname_cstr());
}
if (RuntimeOption::EvalNoticeOnSimpleXMLBehavior) {
raise_notice("SimpleXMLElement to integer cast");
}
return SimpleXMLElement_objectCast(this, KindOfInt64).toInt64();
}
double ObjectData::toDouble() const {
/* SimpleXMLElement is the only class that has proper custom num casting. */
if (LIKELY(!instanceof(SimpleXMLElement_classof()))) {
throwObjToDoubleException(classname_cstr());
}
if (RuntimeOption::EvalNoticeOnSimpleXMLBehavior) {
raise_notice("SimpleXMLElement to double cast");
}
return SimpleXMLElement_objectCast(this, KindOfDouble).toDouble();
}
///////////////////////////////////////////////////////////////////////////////
// instance methods and properties
const StaticString s_getIterator("getIterator");
Object ObjectData::iterableObject(bool& isIterable,
bool mayImplementIterator /* = true */) {
assertx(mayImplementIterator || !isIterator());
if (mayImplementIterator && isIterator()) {
isIterable = true;
return Object(this);
}
Object obj(this);
CoeffectsAutoGuard _;
while (obj->instanceof(SystemLib::s_IteratorAggregateClass)) {
auto iterator =
obj->o_invoke_few_args(s_getIterator, RuntimeCoeffects::automatic(), 0);
if (!iterator.isObject()) break;
auto o = iterator.getObjectData();
if (o->isIterator()) {
isIterable = true;
return Object{o};
}
obj.reset(o);
}
if (!isIterator() && obj->instanceof(SimpleXMLElement_classof())) {
if (RuntimeOption::EvalNoticeOnSimpleXMLBehavior) {
raise_notice("SimpleXMLElement used as iterator");
}
isIterable = true;
return create_object(
s_SimpleXMLElementIterator,
make_vec_array(obj)
);
}
isIterable = false;
return obj;
}
Array& ObjectData::dynPropArray() const {
assertx(getAttribute(HasDynPropArr));
assertx(g_context->dynPropTable.count(this));
assertx(g_context->dynPropTable[this].arr().isDict());
return g_context->dynPropTable[this].arr();
}
void ObjectData::setDynProps(const Array& newArr) {
// don't expose the ref returned by setDynPropArr
(void)setDynPropArray(newArr.toDict());
}
void ObjectData::reserveDynProps(int numDynamic) {
// don't expose the ref returned by reserveProperties()
(void)reserveProperties(numDynamic);
}
Array& ObjectData::reserveProperties(int numDynamic /* = 2 */) {
if (getAttribute(HasDynPropArr)) {
return dynPropArray();
}
auto const allocsz = VanillaDict::computeAllocBytesFromMaxElms(numDynamic);
if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
check_non_safepoint_surprise();
}
return setDynPropArray(Array::attach(
VanillaDict::MakeReserveDict(numDynamic)));
}
Array& ObjectData::setDynPropArray(const Array& newArr) {
assertx(!g_context->dynPropTable.count(this));
assertx(!getAttribute(HasDynPropArr));
assertx(newArr.isDict());
if (m_cls->forbidsDynamicProps()) {
throw_object_forbids_dynamic_props(getClassName().data());
}
if (RuntimeOption::EvalNoticeOnCreateDynamicProp) {
IterateKV(newArr.get(), [&] (TypedValue k, TypedValue v) {
auto const key = tvCastToString(k);
raiseCreateDynamicProp(key.get());
});
}
// newArr can have refcount 2 or higher
auto& arr = g_context->dynPropTable[this].arr();
assertx(arr.isNull());
arr = newArr;
setAttribute(HasDynPropArr);
return arr;
}
tv_lval ObjectData::makeDynProp(const StringData* key) {
if (RuntimeOption::EvalNoticeOnCreateDynamicProp) {
raiseCreateDynamicProp(key);
}
if (!reserveProperties().exists(StrNR(key))) {
reserveProperties().set(StrNR(key), make_tv<KindOfNull>());
}
return reserveProperties().lval(StrNR(key), AccessFlags::Key);
}
void ObjectData::setDynProp(const StringData* key, TypedValue val) {
if (RuntimeOption::EvalNoticeOnCreateDynamicProp) {
raiseCreateDynamicProp(key);
}
reserveProperties().set(StrNR(key), val, true);
}
Variant ObjectData::o_get(const String& propName, bool error /* = true */,
const String& context /*= null_string*/) {
assertx(kindIsValid());
// This is not (just) a check for empty string; property names that start
// with null are intentionally being rejected here.
if (UNLIKELY(!*propName.data())) {
throw_invalid_property_name(propName);
}
Class* ctx = nullptr;
if (!context.empty()) {
ctx = Class::lookup(context.get());
}
// Can't use propImpl here because if the property is not accessible and
// there is no native get, propImpl will raise_error("Cannot access ...",
// but o_get will only (maybe) raise_notice("Undefined property ..." :-(
auto const lookup = getPropImpl<false, true, true>(ctx, propName.get());
if (lookup.val && lookup.accessible) {
if (lookup.val.type() != KindOfUninit) {
return Variant::wrap(lookup.val.tv());
} else if (lookup.prop && (lookup.prop->attrs & AttrLateInit)) {
if (error) throw_late_init_prop(lookup.prop->cls, propName.get(), false);
return uninit_null();
}
}
if (error) {
SystemLib::throwUndefinedPropertyExceptionObject(
folly::sformat("Undefined property: {}::${}",
getClassName().data(),
propName.data()));
}
return uninit_null();
}
void ObjectData::o_set(const String& propName, const Variant& v,
const String& context /* = null_string */) {
assertx(kindIsValid());
// This is not (just) a check for empty string; property names that start
// with null are intentionally being rejected here.
if (UNLIKELY(!*propName.data())) {
throw_invalid_property_name(propName);
}
Class* ctx = nullptr;
if (!context.empty()) {
ctx = Class::lookup(context.get());
}
// Can't use setProp here because if the property is not accessible and
// there is no native set, setProp will raise_error("Cannot access ...",
// but o_set will skip writing and return normally.
auto const lookup = getPropImpl<true, false, true>(ctx, propName.get());
auto prop = lookup.val;
if (prop && lookup.accessible) {
if (UNLIKELY(lookup.isConst) && !isBeingConstructed()) {
throwMutateConstProp(lookup.slot);
}
auto val = tvToInit(*v.asTypedValue());
verifyTypeHint(m_cls, lookup.prop, &val);
tvSet(val, prop);
return;
}
if (!prop) {
setDynProp(propName.get(), tvToInit(*v.asTypedValue()));
}
}
void ObjectData::o_setArray(const Array& properties) {
for (ArrayIter iter(properties); iter; ++iter) {
String k = iter.first().toString();
Class* ctx = nullptr;
// If the key begins with a NUL, it's a private or protected property. Read
// the class name from between the two NUL bytes.
//
// Note: if you change this, you need to change similar logic in
// apc-object.
if (!k.empty() && k[0] == '\0') {
int subLen = k.find('\0', 1) + 1;
String cls = k.substr(1, subLen - 2);
if (cls.size() == 1 && cls[0] == '*') {
// Protected.
ctx = m_cls;
} else {
// Private.
ctx = Class::lookup(cls.get());
if (!ctx) continue;
}
k = k.substr(subLen);
}
setProp(ctx, k.get(), tvAssertPlausible(iter.secondVal()));
}
}
void ObjectData::o_getArray(Array& props,
bool pubOnly /* = false */,
bool ignoreLateInit /* = false */) const {
assertx(kindIsValid());
// Fast path for classes with no declared properties
if (!m_cls->numDeclProperties() && getAttribute(HasDynPropArr)) {
props = dynPropArray();
if (RuntimeOption::EvalNoticeOnReadDynamicProp) {
IterateKV(props.get(), [&](TypedValue k, TypedValue) {
auto const key = tvCastToString(k);
raiseReadDynamicProp(key.get());
});
}
return;
}
auto cls = m_cls;
if (cls->hasReifiedGenerics()) {
auto const slot = cls->lookupReifiedInitProp();
assertx(slot != kInvalidSlot);
auto const declProps = cls->declProperties();
auto const prop = declProps[slot];
auto val = this->propRvalAtOffset(slot);
props.set(StrNR(prop.name).asString(), val.tv());
}
IteratePropToArrayOrder(
this,
[&](Slot slot, const Class::Prop& prop, tv_rval val) {
assertx(assertTypeHint(val, slot));
if (UNLIKELY(val.type() == KindOfUninit)) {
if (!ignoreLateInit && (prop.attrs & AttrLateInit)) {
throw_late_init_prop(prop.cls, prop.name, false);
}
} else if (!pubOnly || (prop.attrs & AttrPublic)) {
// Skip all the reified properties since we already prepended the
// current class' reified property to the list
if (prop.name != s_86reified_prop.get()) {
props.set(StrNR(prop.mangledName).asString(), val.tv());
}
}
},
[&](TypedValue key_tv, TypedValue val) {
props.set(key_tv, val, true);
if (RuntimeOption::EvalNoticeOnReadDynamicProp) {
auto const key = tvCastToString(key_tv);
raiseReadDynamicProp(key.get());
}
}
);
if (m_cls->needsInitThrowable()) {
throwable_mark_array(this, props);
}
}
template <IntishCast IC /* = IntishCast::None */>
Array ObjectData::toArray(bool pubOnly /* = false */,
bool ignoreLateInit /* = false */) const {
assertx(kindIsValid());
// We can quickly tell if this object is a collection, which lets us avoid
// checking for each class in turn if it's not one.
if (isCollection()) {
return collections::toArray<IC>(this);
} else if (UNLIKELY(m_cls->rtAttribute(Class::CallToImpl))) {
// If we end up with other classes that need special behavior, turn the
// assert into an if and add cases.
assertx(instanceof(SimpleXMLElement_classof()));
if (RuntimeOption::EvalNoticeOnSimpleXMLBehavior) {
raise_notice("SimpleXMLElement to array cast");
}
return SimpleXMLElement_darrayCast(this);
} else if (UNLIKELY(instanceof(SystemLib::s_ArrayIteratorClass))) {
SystemLib::throwInvalidOperationExceptionObject(
"ArrayIterator to array cast"
);
not_reached();
} else if (UNLIKELY(instanceof(c_Closure::classof()))) {
return make_vec_array(Object(const_cast<ObjectData*>(this)));
} else if (UNLIKELY(instanceof(DateTimeData::getClass()))) {
return Native::data<DateTimeData>(this)->getDebugInfo();
} else {
auto ret = Array::CreateDict();
o_getArray(ret, pubOnly, ignoreLateInit);
return ret;
}
}
template
Array ObjectData::toArray<IntishCast::None>(bool, bool) const;
template
Array ObjectData::toArray<IntishCast::Cast>(bool, bool) const;
namespace {
size_t getPropertyIfAccessible(ObjectData* obj,
const Class* ctx,
const StringData* key,
Array& properties,
size_t propLeft) {
auto const prop = obj->getProp(ctx, key);
if (prop && prop.type() != KindOfUninit) {
--propLeft;
properties.set(StrNR(key), prop.tv(), true);
}
return propLeft;
}
}
Array ObjectData::o_toIterArray(const String& context) {
if (!m_cls->numDeclProperties()) {
if (getAttribute(HasDynPropArr)) {
auto const props = dynPropArray();
if (RuntimeOption::EvalNoticeOnReadDynamicProp) {
IterateKV(props.get(), [&](TypedValue k, TypedValue) {
auto const key = tvCastToString(k);
raiseReadDynamicProp(key.get());
});
}
// not returning Array&; makes a copy
return props;
}
return Array::CreateDict();
}
size_t accessibleProps = m_cls->declPropNumAccessible();
size_t size = accessibleProps;
if (getAttribute(HasDynPropArr)) {
size += dynPropArray().size();
}
Array retArray { Array::attach(VanillaDict::MakeReserveDict(size)) };
Class* ctx = nullptr;
if (!context.empty()) {
ctx = Class::lookup(context.get());
}
// Get all declared properties first, bottom-to-top in the inheritance
// hierarchy, in declaration order.
const Class* klass = m_cls;
while (klass) {
const PreClass::Prop* props = klass->preClass()->properties();
const size_t numProps = klass->preClass()->numProperties();
for (size_t i = 0; i < numProps; ++i) {
auto key = const_cast<StringData*>(props[i].name());
accessibleProps = getPropertyIfAccessible(
this, ctx, key, retArray, accessibleProps);
}
klass = klass->parent();
}
if (!(m_cls->attrs() & AttrNoExpandTrait) && accessibleProps > 0) {
// we may have properties from traits
for (auto const& prop : m_cls->declProperties()) {
auto const key = prop.name.get();
if (!retArray.get()->exists(key)) {
accessibleProps = getPropertyIfAccessible(
this, ctx, key, retArray, accessibleProps);
if (accessibleProps == 0) break;
}
}
}
// Now get dynamic properties.
if (getAttribute(HasDynPropArr)) {
auto& dynProps = dynPropArray();
auto ad = dynProps.get();
ssize_t iter = ad->iter_begin();
auto pos_limit = ad->iter_end();
while (iter != pos_limit) {
ad = dynProps.get();
auto const key = ad->nvGetKey(iter);
iter = ad->iter_advance(iter);
if (RuntimeOption::EvalNoticeOnReadDynamicProp) {
auto const k = tvCastToString(key);
raiseReadDynamicProp(k.get());
}
// You can get this if you cast an array to object. These
// properties must be dynamic because you can't declare a
// property with a non-string name.
if (UNLIKELY(!isStringType(key.m_type))) {
assertx(key.m_type == KindOfInt64);
auto const val = dynProps.get()->at(key.m_data.num);
retArray.set(key.m_data.num, val);
continue;
}
auto const strKey = key.m_data.pstr;
auto const val = dynProps.get()->at(strKey);
retArray.set(StrNR(strKey), val, true /* isKey */);
}
}
return retArray;
}
static bool decode_invoke(const String& s, ObjectData* obj, bool fatal,
CallCtx& ctx) {
ctx.this_ = obj;
ctx.cls = obj->getVMClass();
ctx.dynamic = true;
ctx.func = ctx.cls->lookupMethod(s.get());
if (!ctx.func) {
// Bail if this_ is non-null AND we could not find a method.
o_invoke_failed(ctx.cls->name()->data(), s.data(), fatal);
return false;
}
// Null out this_ for statically called methods
if (ctx.func->isStaticInPrologue()) {
ctx.this_ = nullptr;
}
return true;
}
Variant ObjectData::o_invoke(const String& s, const Variant& params,
bool fatal /* = true */) {
CallCtx ctx;
if (!decode_invoke(s, this, fatal, ctx) ||
(!isContainer(params) && !params.isNull())) {
return Variant(Variant::NullInit());
}
CoeffectsAutoGuard _;
return Variant::attach(
g_context->invokeFunc(ctx, params, RuntimeCoeffects::automatic())
);
}
Variant ObjectData::o_invoke_few_args(const String& s,
RuntimeCoeffects providedCoeffects,
int count,
const Variant& a0 /* = uninit_variant*/,
const Variant& a1 /* = uninit_variant*/,
const Variant& a2 /* = uninit_variant*/,
const Variant& a3 /* = uninit_variant*/,
const Variant& a4 /* = uninit_variant*/) {
CallCtx ctx;
if (!decode_invoke(s, this, true, ctx)) {
return Variant(Variant::NullInit());
}
TypedValue args[5];
switch(count) {
default: not_implemented();
case 5: tvCopy(*a4.asTypedValue(), args[4]);
case 4: tvCopy(*a3.asTypedValue(), args[3]);
case 3: tvCopy(*a2.asTypedValue(), args[2]);
case 2: tvCopy(*a1.asTypedValue(), args[1]);
case 1: tvCopy(*a0.asTypedValue(), args[0]);
case 0: break;
}
return Variant::attach(
g_context->invokeFuncFew(ctx, count, args, providedCoeffects)
);
}
ObjectData* ObjectData::clone() {
if (isCppBuiltin()) {
assertx(!m_cls->hasMemoSlots());
if (isCollection()) return collections::clone(this);
if (instanceof(c_Closure::classof())) {
return c_Closure::fromObject(this)->clone();
}
assertx(instanceof(c_Awaitable::classof()));
// cloning WaitHandles is not allowed
// invoke the instanceCtor to get the right sort of exception
auto const ctor = m_cls->instanceCtor();
ctor(m_cls);
always_assert(false);
}
// clone prevents a leak if something throws before clone() returns
Object clone;
auto const nProps = m_cls->numDeclProperties();
if (hasNativeData()) {
assertx(m_cls->instanceDtor() == Native::nativeDataInstanceDtor);
clone = Object::attach(
Native::nativeDataInstanceCopyCtor(this, m_cls, nProps)
);
assertx(clone->hasExactlyOneRef());
assertx(clone->hasInstanceDtor());
} else {
auto const alloc = allocMemoInit(m_cls);
auto const obj = new (NotNull{}, alloc.mem)
ObjectData(m_cls, InitRaw{}, alloc.flags);
clone = Object::attach(obj);
assertx(clone->hasExactlyOneRef());
assertx(!clone->hasInstanceDtor());
}
auto const cloneProps = clone->props();
cloneProps->init(m_cls->numDeclProperties());
for (auto slot = Slot{0}; slot < nProps; slot++) {
auto index = m_cls->propSlotToIndex(slot);
tvDup(*props()->at(index), cloneProps->at(index));
assertx(assertTypeHint(cloneProps->at(index), slot));
}
if (UNLIKELY(getAttribute(HasDynPropArr))) {
clone->setAttribute(HasDynPropArr);
g_context->dynPropTable.emplace(clone.get(), dynPropArray().get());
}
if (m_cls->rtAttribute(Class::HasClone)) {
assertx(!isCppBuiltin());
auto const method = clone->m_cls->lookupMethod(s_clone.get());
assertx(method);
clone->unlockObject();
SCOPE_EXIT { clone->lockObject(); };
CoeffectsAutoGuard _;
g_context->invokeMethodV(clone.get(), method, InvokeArgs{},
RuntimeCoeffects::automatic());
}
return clone.detach();
}
bool ObjectData::equal(const ObjectData& other) const {
if (this == &other) return true;
if (getVMClass() == SystemLib::s_HH_SwitchableClassClass ||
other.getVMClass() == SystemLib::s_HH_SwitchableClassClass) {
return false; // Never compare for structural equality.
}
if (isCollection()) {
return collections::equals(this, &other);
}
if (UNLIKELY(instanceof(SystemLib::s_DateTimeInterfaceClass) &&
other.instanceof(SystemLib::s_DateTimeInterfaceClass))) {
return DateTimeData::compare(this, &other) == 0;
}
if (getVMClass() != other.getVMClass()) return false;
if (UNLIKELY(instanceof(SimpleXMLElement_classof()))) {
if (RuntimeOption::EvalNoticeOnSimpleXMLBehavior) {
raise_notice("SimpleXMLElement equality comparison");
}
// Compare the whole object (including native data), not just props
auto ar1 = SimpleXMLElement_darrayCast(this);
auto ar2 = SimpleXMLElement_darrayCast(&other);
return ArrayData::Equal(ar1.get(), ar2.get());
}
if (UNLIKELY(instanceof(c_Closure::classof()))) {
// First comparison already proves they are different
return false;
}
// check for dynamic props first because we need to short-circuit if there's
// a different number of them
auto thisSize = UNLIKELY(getAttribute(HasDynPropArr)) ?
dynPropArray().size() : 0;
size_t otherSize = 0;
ArrayData* otherDynProps = nullptr;
if (UNLIKELY(other.getAttribute(HasDynPropArr))) {
otherDynProps = other.dynPropArray().get();
otherSize = otherDynProps->size();
}
if (thisSize != otherSize) return false;
// Prevent circular referenced objects/arrays or deep ones.
check_recursion_error();
bool result = true;
IteratePropMemOrder(
this,
[&](Slot slot, const Class::Prop& prop, tv_rval thisVal) {
auto otherVal = other.propRvalAtOffset(slot);
if ((UNLIKELY(thisVal.type() == KindOfUninit) ||
UNLIKELY(otherVal.type() == KindOfUninit)) &&
(prop.attrs & AttrLateInit)) {
throw_late_init_prop(prop.cls, prop.name, false);
}
if (!tvEqual(thisVal.tv(), otherVal.tv())) {
result = false;
return true;
}
return false;
},
[&](TypedValue key, TypedValue thisVal) {
auto const otherVal = otherDynProps->get(key);
if (!otherVal.is_init() || !tvEqual(thisVal, otherVal)) {
result = false;
return true;
}
return false;
}
);
return result;
}
bool ObjectData::less(const ObjectData& other) const {
// compare is not symmetrical; order of operands matters here
return compare(other) < 0;
}
bool ObjectData::lessEqual(const ObjectData& other) const {
// compare is not symmetrical; order of operands matters here
return compare(other) <= 0;
}
bool ObjectData::more(const ObjectData& other) const {
// compare is not symmetrical; order of operands matters here
return other.compare(*this) < 0;
}
bool ObjectData::moreEqual(const ObjectData& other) const {
// compare is not symmetrical; order of operands matters here
return other.compare(*this) <= 0;
}
int64_t ObjectData::compare(const ObjectData& other) const {
if (isCollection() || other.isCollection()) {
throw_collection_compare_exception();
}
if (this == &other) return 0;
if (UNLIKELY(instanceof(SystemLib::s_DateTimeInterfaceClass) &&
other.instanceof(SystemLib::s_DateTimeInterfaceClass))) {
return DateTimeData::compare(this, &other);
}
if (getVMClass() != other.getVMClass()) {
const auto lhs = make_tv<DataType::Object>(const_cast<ObjectData*>(this));
const auto rhs = make_tv<DataType::Object>(const_cast<ObjectData*>(&other));
throwCmpBadTypesException(&lhs, &rhs);
not_reached();
}
if (UNLIKELY(instanceof(SimpleXMLElement_classof()))) {
if (RuntimeOption::EvalNoticeOnSimpleXMLBehavior) {
raise_notice("SimpleXMLElement comparison");
}
// Compare the whole object (including native data), not just props
auto ar1 = SimpleXMLElement_darrayCast(this);
auto ar2 = SimpleXMLElement_darrayCast(&other);
return ArrayData::Compare(ar1.get(), ar2.get());
}
if (UNLIKELY(instanceof(c_Closure::classof()))) {
// comparing different closures with <=> always returns 1
return 1;
}
// check for dynamic props first, because we need to short circuit if there's
// a different number of them
auto thisSize = UNLIKELY(getAttribute(HasDynPropArr)) ?
dynPropArray().size() : 0;
size_t otherSize = 0;
ArrayData* otherDynProps = nullptr;
if (UNLIKELY(other.getAttribute(HasDynPropArr))) {
otherDynProps = other.dynPropArray().get();
otherSize = otherDynProps->size();
}
if (thisSize > otherSize) {
return 1;
} else if (thisSize < otherSize) {
return -1;
}
// Prevent circular referenced objects/arrays or deep ones.
check_recursion_error();
int64_t result = 0;
IteratePropToArrayOrder(
this,
[&](Slot slot, const Class::Prop& prop, tv_rval thisVal) {
auto otherVal = other.propRvalAtOffset(slot);
if ((UNLIKELY(thisVal.type() == KindOfUninit) ||
UNLIKELY(otherVal.type() == KindOfUninit)) &&
(prop.attrs & AttrLateInit)) {
throw_late_init_prop(prop.cls, prop.name, false);
}
auto cmp = tvCompare(thisVal.tv(), otherVal.tv());
if (cmp != 0) {
result = cmp;
return true;
}
return false;
},
[&](TypedValue key, TypedValue thisVal) {
auto const otherVal = otherDynProps->get(key);
if (!otherVal.is_init()) {
result = 1;
return true;
}
auto cmp = tvCompare(thisVal, otherVal);
if (cmp != 0) {
result = cmp;
return true;
}
return false;
}
);
return result;
}
///////////////////////////////////////////////////////////////////////////////
const StaticString
s___sleep("__sleep"),
s___toDebugDisplay("__toDebugDisplay"),
s___wakeup("__wakeup"),
s___debugInfo("__debugInfo");
void deepInitHelper(ObjectProps* props,
const Class::PropInitVec* initVec,
size_t nProps) {
auto initIter = initVec->cbegin();
props->init(nProps);
props->foreach(nProps, [&](tv_lval lval){
auto entry = *initIter++;
tvCopy(entry.val.tv(), lval);
if (entry.deepInit) {
tvIncRefGen(*lval);
collections::deepCopy(lval);
}
});
}
void ObjectData::setReifiedGenerics(Class* cls, ArrayData* reifiedTypes) {
auto const arg = make_array_like_tv(reifiedTypes);
auto const meth = cls->lookupMethod(s_86reifiedinit.get());
assertx(meth != nullptr);
g_context->invokeMethod(this, meth, InvokeArgs(&arg, 1),
RuntimeCoeffects::fixme());
}
// called from jit code
ObjectData* ObjectData::newInstanceRawSmall(Class* cls, size_t size,
size_t index) {
assertx(size <= kMaxSmallSize);
assertx(!cls->hasMemoSlots());
assertx(cls->sizeIdx() == index);
auto mem = tl_heap->mallocSmallIndexSize(index, size);
auto const flags = IsBeingConstructed | SmallAllocSize;
return new (NotNull{}, mem) ObjectData(cls, InitRaw{}, flags);
}
ObjectData* ObjectData::newInstanceRawBig(Class* cls, size_t size) {
assertx(!cls->hasMemoSlots());
auto mem = tl_heap->mallocBigSize(size);
auto const flags = IsBeingConstructed | BigAllocSize;
return new (NotNull{}, mem) ObjectData(cls, InitRaw{}, flags);
}
// called from jit code
ObjectData* ObjectData::newInstanceRawMemoSmall(Class* cls,
size_t size,
size_t index,
size_t objoff) {
assertx(size <= kMaxSmallSize);
assertx(cls->hasMemoSlots());
assertx(!cls->getNativeDataInfo());
assertx(objoff == ObjectData::objOffFromMemoNode(cls));
assertx(cls->sizeIdx() == index);
auto mem = tl_heap->mallocSmallIndexSize(index, size);
new (NotNull{}, mem) MemoNode(objoff);
mem = reinterpret_cast<char*>(mem) + objoff;
auto const flags = IsBeingConstructed | SmallAllocSize;
return new (NotNull{}, mem) ObjectData(cls, InitRaw{}, flags);
}
ObjectData* ObjectData::newInstanceRawMemoBig(Class* cls,
size_t size,
size_t objoff) {
assertx(cls->hasMemoSlots());
assertx(!cls->getNativeDataInfo());
assertx(objoff == ObjectData::objOffFromMemoNode(cls));
auto mem = tl_heap->mallocBigSize(size);
new (NotNull{}, mem) MemoNode(objoff);
mem = reinterpret_cast<char*>(mem) + objoff;
auto const flags = IsBeingConstructed | BigAllocSize;
return new (NotNull{}, mem) ObjectData(cls, InitRaw{}, flags);
}
// Note: the normal object destruction path does not actually call this
// destructor. See ObjectData::release.
ObjectData::~ObjectData() {
if (UNLIKELY(slowDestroyCheck())) {
// The only builtin classes that use ~ObjectData and support memoization
// are ones with native data, and the memo slot cleanup for them happens
// in nativeDataInstanceDtor.
assertx(!getAttribute(UsedMemoCache) || hasNativeData());
if (getAttribute(HasDynPropArr)) freeDynPropArray(this);
if (getAttribute(IsWeakRefed)) {
WeakRefData::invalidateWeakRef((uintptr_t)this);
}
}
}
Object ObjectData::FromArray(ArrayData* properties) {
auto const props = properties->toDict(true);
Object retval{SystemLib::s_stdclassClass};
retval->setAttribute(HasDynPropArr);
g_context->dynPropTable.emplace(retval.get(), props);
if (props != properties) decRefArr(props);
return retval;
}
void ObjectData::throwMutateConstProp(Slot prop) const {
throw_cannot_modify_const_prop(
getClassName().data(),
m_cls->declProperties()[prop].name->data()
);
}
void ObjectData::throwMustBeMutable(Slot prop) const {
throw_must_be_mutable(
getClassName().data(),
m_cls->declProperties()[prop].name->data()
);
}
void ObjectData::throwMustBeEnclosedInReadonly(Slot prop) const {
throw_must_be_enclosed_in_readonly(
getClassName().data(),
m_cls->declProperties()[prop].name->data()
);
}
void ObjectData::throwMustBeReadonly(Slot prop) const {
throw_must_be_readonly(
getClassName().data(),
m_cls->declProperties()[prop].name->data()
);
}
void ObjectData::throwMustBeValueType(Slot prop) const {
throw_must_be_value_type(
getClassName().data(),
m_cls->declProperties()[prop].name->data()
);
}
void ObjectData::checkReadonly(const PropLookup& lookup, ReadonlyOp op,
bool writeMode) const {
if ((op == ReadonlyOp::CheckMutROCOW && lookup.readonly) ||
op == ReadonlyOp::CheckROCOW) {
vmMInstrState().roProp = true;
}
if (lookup.readonly) {
if (op == ReadonlyOp::CheckMutROCOW || op == ReadonlyOp::CheckROCOW) {
if (type(lookup.val) == KindOfObject) {
throwMustBeValueType(lookup.slot);
}
} else if (op == ReadonlyOp::Mutable) {
if (writeMode) {
throwMustBeMutable(lookup.slot);
} else {
throwMustBeEnclosedInReadonly(lookup.slot);
}
}
} else if (op == ReadonlyOp::Readonly || op == ReadonlyOp::CheckROCOW) {
throwMustBeReadonly(lookup.slot);
}
}
template <bool forWrite, bool forRead, bool ignoreLateInit>
ALWAYS_INLINE
ObjectData::PropLookup ObjectData::getPropImpl(
const Class* ctx,
const StringData* key
) {
auto const lookup = m_cls->getDeclPropSlot(ctx, key);
auto const propSlot = lookup.slot;
if (LIKELY(propSlot != kInvalidSlot)) {
// We found a visible property, but it might not be accessible. No need to
// check if there is a dynamic property with this name.
auto const propIndex = m_cls->propSlotToIndex(propSlot);
auto prop = props()->at(propIndex);
assertx(assertTypeHint(prop, propSlot));
auto const& declProp = m_cls->declProperties()[propSlot];
if (!ignoreLateInit && lookup.accessible) {
if (UNLIKELY(type(prop) == KindOfUninit) &&
(declProp.attrs & AttrLateInit)) {
throw_late_init_prop(declProp.cls, key, false);
}
}
return {
prop,
&declProp,
propSlot,
lookup.accessible,
// we always return true in the !forWrite case; this way the compiler
// may optimize away this value, and if a caller intends to write but
// instantiates with false by mistake it will always see const
forWrite
? bool(declProp.attrs & AttrIsConst)
: true,
lookup.readonly
};
}
// We could not find a visible declared property. We need to check for a
// dynamic property with this name.
if (UNLIKELY(getAttribute(HasDynPropArr))) {
auto& arr = dynPropArray();
if (arr->exists(key)) {
if (forRead && RuntimeOption::EvalNoticeOnReadDynamicProp) {
raiseReadDynamicProp(key);
}
// Returning a non-declared property. We know that it is accessible and
// not const since all dynamic properties are. If we may write to
// the property we need to allow the array to escalate.
auto const lval = arr.lval(StrNR(key), AccessFlags::Key);
return { lval, nullptr, kInvalidSlot, true, !forWrite, false };
}
}
return { nullptr, nullptr, kInvalidSlot, false, !forWrite, false };
}
tv_lval ObjectData::getPropLval(const Class* ctx, const StringData* key) {
auto const lookup = getPropImpl<true, false, true>(ctx, key);
if (UNLIKELY(lookup.isConst) && !isBeingConstructed()) {
throwMutateConstProp(lookup.slot);
}
return lookup.val && lookup.accessible ? lookup.val : nullptr;
}
tv_rval ObjectData::getProp(const Class* ctx, const StringData* key) const {
auto const lookup = const_cast<ObjectData*>(this)
->getPropImpl<false, true, false>(ctx, key);
return lookup.val && lookup.accessible ? lookup.val : nullptr;
}
tv_rval ObjectData::getPropIgnoreLateInit(const Class* ctx,
const StringData* key) const {
auto const lookup = const_cast<ObjectData*>(this)
->getPropImpl<false, true, true>(ctx, key);
return lookup.val && lookup.accessible ? lookup.val : nullptr;
}
tv_lval ObjectData::getPropIgnoreAccessibility(const StringData* key) {
auto const lookup = getPropImpl<false, true, true>(nullptr, key);
auto prop = lookup.val;
if (!prop) return nullptr;
if (lookup.prop && type(prop) == KindOfUninit &&
(lookup.prop->attrs & AttrLateInit)) {
throw_late_init_prop(lookup.prop->cls, key, false);
}
return prop;
}
//////////////////////////////////////////////////////////////////////
template<ObjectData::PropMode mode>
ALWAYS_INLINE
tv_lval ObjectData::propImpl(TypedValue* tvRef, const Class* ctx,
const StringData* key, const ReadonlyOp op) {
auto constexpr write = (mode == PropMode::DimForWrite);
auto constexpr read = (mode == PropMode::ReadNoWarn) ||
(mode == PropMode::ReadWarn);
auto const lookup = getPropImpl<write, read, false>(ctx, key);
auto const prop = lookup.val;
if (prop) {
if (lookup.accessible) {
auto const checkPropAttrs = [&]() {
if (mode == PropMode::DimForWrite) {
if (UNLIKELY(lookup.isConst) && !isBeingConstructed()) {
throwMutateConstProp(lookup.slot);
}
}
checkReadonly(lookup, op, mode == PropMode::DimForWrite);
return prop;
};
// Property exists, is accessible, and is not unset.
if (type(prop) != KindOfUninit) return checkPropAttrs();
if (mode == PropMode::ReadWarn) throwUndefPropException(key);
if (write) return checkPropAttrs();
return const_cast<TypedValue*>(&immutable_null_base);
}
// Property exists, but it is either protected or private since accessible
// is false.
auto const propSlot = m_cls->lookupDeclProp(key);
auto const attrs = m_cls->declProperties()[propSlot].attrs;
auto const priv = (attrs & AttrPrivate) ? "private" : "protected";
raise_error(
"Cannot access %s property %s::$%s",
priv,
m_cls->preClass()->name()->data(),
key->data()
);
}
// First see if native getter is implemented.
if (m_cls->rtAttribute(Class::HasNativePropHandler)) {
auto r = Native::getProp(Object{this}, StrNR(key));
if (r.isInitialized()) {
tvCopy(r.detach(), *tvRef);
return tvRef;
}
}
if (UNLIKELY(!*key->data())) {
throw_invalid_property_name(StrNR(key));
}
if (mode == PropMode::ReadWarn) throwUndefPropException(key);
if (write) return makeDynProp(key);
return const_cast<TypedValue*>(&immutable_null_base);
}
tv_lval ObjectData::prop(
TypedValue* tvRef,
const Class* ctx,
const StringData* key,
const ReadonlyOp op
) {
return propImpl<PropMode::ReadNoWarn>(tvRef, ctx, key, op);
}
tv_lval ObjectData::propW(
TypedValue* tvRef,
const Class* ctx,
const StringData* key,
const ReadonlyOp op
) {
return propImpl<PropMode::ReadWarn>(tvRef, ctx, key, op);
}
tv_lval ObjectData::propU(
TypedValue* tvRef,
const Class* ctx,
const StringData* key,
const ReadonlyOp op
) {
return propImpl<PropMode::DimForWrite>(tvRef, ctx, key, op);
}
tv_lval ObjectData::propD(
TypedValue* tvRef,
const Class* ctx,
const StringData* key,
const ReadonlyOp op
) {
return propImpl<PropMode::DimForWrite>(tvRef, ctx, key, op);
}
bool ObjectData::propIsset(const Class* ctx, const StringData* key) {
auto const lookup = getPropImpl<false, true, true>(ctx, key);
if (lookup.val && lookup.accessible) {
if (lookup.val.type() != KindOfUninit) {
return lookup.val.type() != KindOfNull;
}
if (lookup.prop && (lookup.prop->attrs & AttrLateInit)) {
return false;
}
}
if (m_cls->rtAttribute(Class::HasNativePropHandler)) {
auto r = Native::issetProp(Object{this}, StrNR(key));
if (r.isInitialized()) return r.toBoolean();
}
return false;
}
void ObjectData::setProp(Class* ctx, const StringData* key, TypedValue val, ReadonlyOp op) {
assertx(tvIsPlausible(val));
assertx(val.m_type != KindOfUninit);
auto const lookup = getPropImpl<true, false, true>(ctx, key);
auto const prop = lookup.val;
if (prop && lookup.accessible) {
if (UNLIKELY(lookup.isConst) && !isBeingConstructed()) {
throwMutateConstProp(lookup.slot);
}
checkReadonly(lookup, op, true);
// TODO(T61738946): We can remove the temporary here once we no longer
// coerce class_meth types.
Variant tmp = tvAsVariant(&val);
verifyTypeHint(m_cls, lookup.prop, tmp.asTypedValue());
tvMove(tmp.detach(), prop);
return;
}
// First see if native setter is implemented.
if (m_cls->rtAttribute(Class::HasNativePropHandler)) {
auto r = Native::setProp(Object{this}, StrNR(key), tvAsCVarRef(&val));
if (r.isInitialized()) return;
}
if (prop) raise_error("Cannot access protected property");
if (UNLIKELY(!*key->data())) {
throw_invalid_property_name(StrNR(key));
}
setDynProp(key, val);
}
tv_lval ObjectData::setOpProp(TypedValue& tvRef,
Class* ctx,
SetOpOp op,
const StringData* key,
TypedValue* val) {
auto const lookup = getPropImpl<true, true, false>(ctx, key);
auto prop = lookup.val;
if (prop && lookup.accessible) {
if (UNLIKELY(lookup.isConst) && !isBeingConstructed()) {
throwMutateConstProp(lookup.slot);
}
auto const needsCheck = lookup.prop && [&] {
auto const& tc = lookup.prop->typeConstraint;
if (setOpNeedsTypeCheck(tc, op, prop)) {
return true;
}
for (auto& ub : lookup.prop->ubs) {
if (setOpNeedsTypeCheck(ub, op, prop)) return true;
}
return false;
}();
if (needsCheck) {
/*
* If this property has a type-hint, we can't do the setop truly in
* place. We need to verify that the new value satisfies the type-hint
* before assigning back to the property (if we raise a warning and throw,
* we don't want to have already put the value into the prop).
*/
TypedValue temp;
tvDup(*prop, temp);
SCOPE_FAIL { tvDecRefGen(&temp); };
setopBody(&temp, op, val);
verifyTypeHint(m_cls, lookup.prop, &temp);
tvMove(temp, prop);
} else {
setopBody(prop, op, val);
}
return prop;
}
if (UNLIKELY(!*key->data())) throw_invalid_property_name(StrNR(key));
// Native accessors.
if (m_cls->rtAttribute(Class::HasNativePropHandler)) {
auto r = Native::getProp(Object{this}, StrNR(key));
if (r.isInitialized()) {
setopBody(r.asTypedValue(), op, val);
auto r2 = Native::setProp(Object{this}, StrNR(key), r);
if (r2.isInitialized()) {
tvCopy(r.detach(), tvRef);
return &tvRef;
}
}
}
if (prop) raise_error("Cannot access protected property");
// No visible/accessible property, and no applicable native method:
// create a new dynamic property. (We know this is a new property,
// or it would've hit the visible && accessible case above.)
prop = makeDynProp(key);
assertx(type(prop) == KindOfNull); // cannot exist yet
setopBody(prop, op, val);
return prop;
}
TypedValue ObjectData::incDecProp(Class* ctx, IncDecOp op, const StringData* key) {
auto const lookup = getPropImpl<true, true, false>(ctx, key);
auto prop = lookup.val;
if (prop && lookup.accessible) {
if (UNLIKELY(lookup.isConst) && !isBeingConstructed()) {
throwMutateConstProp(lookup.slot);
}
if (type(prop) == KindOfUninit) {
tvWriteNull(prop);
}
/*
* If this property has a type-hint, we can't do the inc-dec truely in
* place. We need to verify that the new value satisfies the type-hint
* before assigning back to the property (if we raise a warning and throw,
* we don't want to have already put the value into the prop).
*
* If the prop is an integer and we're doing the common pre/post inc/dec
* ops, we know the type won't change, so we can skip the type-hint check in
* that case.
*/
auto const fast = [&]{
if (RuntimeOption::EvalCheckPropTypeHints <= 0) return true;
auto const isAnyCheckable = lookup.prop && [&] {
if (lookup.prop->typeConstraint.isCheckable()) return true;
for (auto const& ub : lookup.prop->ubs) {
if (ub.isCheckable()) return true;
}
return false;
}();
if (!isAnyCheckable) return true;
if (!isIntType(type(prop))) return false;
return
op == IncDecOp::PreInc || op == IncDecOp::PostInc ||
op == IncDecOp::PreDec || op == IncDecOp::PostDec;
}();
if (fast) return IncDecBody(op, tvAssertPlausible(prop));
TypedValue temp;
tvDup(tvAssertPlausible(*prop), temp);
SCOPE_FAIL { tvDecRefGen(&temp); };
auto result = IncDecBody(op, &temp);
SCOPE_FAIL { tvDecRefGen(&result); };
verifyTypeHint(m_cls, lookup.prop, &temp);
tvMove(temp, tvAssertPlausible(prop));
return result;
}
if (UNLIKELY(!*key->data())) throw_invalid_property_name(StrNR(key));
// Native accessors.
if (m_cls->rtAttribute(Class::HasNativePropHandler)) {
auto r = Native::getProp(Object{this}, StrNR(key));
if (r.isInitialized()) {
auto const dest = IncDecBody(op, r.asTypedValue());
auto r2 = Native::setProp(Object{this}, StrNR(key), r);
if (r2.isInitialized()) return dest;
}
}
if (prop) raise_error("Cannot access protected property");
// No visible/accessible property, and no applicable native method:
// create a new dynamic property. (We know this is a new property,
// or it would've hit the visible && accessible case above.)
prop = makeDynProp(key);
assertx(type(prop) == KindOfNull); // cannot exist yet
return IncDecBody(op, prop);
}
void ObjectData::unsetProp(Class* ctx, const StringData* key) {
auto const lookup = getPropImpl<true, false, true>(ctx, key);
auto const prop = lookup.val;
if (prop && lookup.accessible &&
(type(prop) != KindOfUninit ||
(lookup.prop && (lookup.prop->attrs & AttrLateInit)))) {
if (lookup.slot != kInvalidSlot) {
// Declared property.
if (UNLIKELY(lookup.isConst) && !isBeingConstructed()) {
throwMutateConstProp(lookup.slot);
}
unsetTypeHint(lookup.prop);
tvSet(*uninit_variant.asTypedValue(), prop);
} else {
// Dynamic property.
dynPropArray().remove(StrNR(key).asString(), true /* isString */);
}
return;
}
// Native unset first.
if (m_cls->rtAttribute(Class::HasNativePropHandler)) {
auto r = Native::unsetProp(Object{this}, StrNR(key));
if (r.isInitialized()) return;
}
if (prop && !lookup.accessible) {
// Defined property that is not accessible.
raise_error("Cannot unset inaccessible property");
}
if (UNLIKELY(!*key->data())) {
throw_invalid_property_name(StrNR(key));
}
}
void ObjectData::throwObjToIntException(const char* clsName) {
SystemLib::throwTypecastExceptionObject(folly::sformat(
"Object of class {} could not be converted to int", clsName));
}
void ObjectData::throwObjToDoubleException(const char* clsName) {
SystemLib::throwTypecastExceptionObject(folly::sformat(
"Object of class {} could not be converted to float", clsName));
}
void ObjectData::raiseAbstractClassError(Class* cls) {
Attr attrs = cls->attrs();
raise_error("Cannot instantiate %s %s",
(attrs & AttrInterface) ? "interface" :
(attrs & AttrTrait) ? "trait" :
(attrs & (AttrEnum|AttrEnumClass)) ? "enum" : "abstract class",
cls->preClass()->name()->data());
}
void ObjectData::throwUndefPropException(const StringData* key) const {
SystemLib::throwUndefinedPropertyExceptionObject(
folly::sformat("Undefined property: {}::${}",
m_cls->name()->data(),
key->data()));
}
void ObjectData::raiseCreateDynamicProp(const StringData* key) const {
if (m_cls == SystemLib::s_stdclassClass ||
m_cls == SystemLib::s___PHP_Incomplete_ClassClass) {
// these classes (but not classes derived from them) don't get notices
return;
}
if (key->isStatic()) {
raise_notice("Created dynamic property with static name %s::%s",
m_cls->name()->data(), key->data());
} else {
raise_notice("Created dynamic property with dynamic name %s::%s",
m_cls->name()->data(), key->data());
}
}
void ObjectData::raiseReadDynamicProp(const StringData* key) const {
if (m_cls == SystemLib::s_stdclassClass ||
m_cls == SystemLib::s___PHP_Incomplete_ClassClass) {
// these classes (but not classes derived from them) don't get notices
return;
}
if (key->isStatic()) {
raise_notice("Read dynamic property with static name %s::%s",
m_cls->name()->data(), key->data());
} else {
raise_notice("Read dynamic property with dynamic name %s::%s",
m_cls->name()->data(), key->data());
}
}
void ObjectData::raiseImplicitInvokeToString() const {
raise_notice("Implicitly invoked %s::__toString", m_cls->name()->data());
}
Variant ObjectData::InvokeSimple(ObjectData* obj, const StaticString& name,
RuntimeCoeffects providedCoeffects) {
auto const meth = obj->methodNamed(name.get());
return meth
? g_context->invokeMethodV(obj, meth, InvokeArgs{}, providedCoeffects)
: uninit_null();
}
Variant ObjectData::invokeSleep(RuntimeCoeffects provided) {
return InvokeSimple(this, s___sleep, provided);
}
Variant ObjectData::invokeToDebugDisplay(RuntimeCoeffects provided) {
return InvokeSimple(this, s___toDebugDisplay, provided);
}
Variant ObjectData::invokeWakeup(RuntimeCoeffects provided) {
unlockObject();
SCOPE_EXIT { lockObject(); };
return InvokeSimple(this, s___wakeup, provided);
}
Variant ObjectData::invokeDebugInfo(RuntimeCoeffects provided) {
return InvokeSimple(this, s___debugInfo, provided);
}
String ObjectData::invokeToString() {
if (RuntimeOption::EvalFatalOnConvertObjectToString) {
raise_convert_object_to_string(classname_cstr());
}
const Func* method = m_cls->getToString();
if (!method) {
// If the object does not define a __toString() method, raise a
// recoverable error
raise_recoverable_error(
"Object of class %s could not be converted to string",
classname_cstr()
);
// If the user error handler decides to allow execution to continue,
// we return the empty string.
return empty_string();
}
if (RuntimeOption::EvalNoticeOnImplicitInvokeToString) {
raiseImplicitInvokeToString();
}
CoeffectsAutoGuard _;
auto const tv = g_context->invokeMethod(this, method, InvokeArgs{},
RuntimeCoeffects::automatic());
if (!isStringType(tv.m_type) &&
!isClassType(tv.m_type) &&
!isLazyClassType(tv.m_type)) {
// Discard the value returned by the __toString() method and raise
// a recoverable error
tvDecRefGen(tv);
raise_recoverable_error(
"Method %s::__toString() must return a string value",
m_cls->preClass()->name()->data());
// If the user error handler decides to allow execution to continue,
// we return the empty string.
return empty_string();
}
if (tvIsString(tv)) return String::attach(val(tv).pstr);
if (tvIsLazyClass(tv)) {
return StrNR{lazyClassToStringHelper(tv.m_data.plazyclass)};
}
assertx(isClassType(type(tv)));
return StrNR(classToStringHelper(tv.m_data.pclass));
}
bool ObjectData::hasToString() {
return (m_cls->getToString() != nullptr);
}
const char* ObjectData::classname_cstr() const {
return getClassName().data();
}
} // HPHP