-
Notifications
You must be signed in to change notification settings - Fork 6k
Moved memory management code from scoped_nsobject from the header to the binary #18362
Conversation
|
@chinmaygarde is the outstanding concern that this may break the engine? Or that the tests might break? If if made you more comfortable I could make this a complete noop for the engine: template <typename NST, typename MemoryManagement = scoped_nsprotocol_memory_management<NST>>
class scoped_nsprotocol {
public:
explicit scoped_nsprotocol(NST object = nil) : object_(object) {}
scoped_nsprotocol(const scoped_nsprotocol<NST>& that) : object_(MemoryManagement::retain(that.object_)) {}
template <typename NSU>
scoped_nsprotocol(const scoped_nsprotocol<NSU>& that) : object_(MemoryManagement::retain(that.get())) {}
~scoped_nsprotocol() { MemoryManagement::release(object_); }
scoped_nsprotocol& operator=(const scoped_nsprotocol<NST>& that) {
reset(MemoryManagement::retain(that.get()));
return *this;
}
...For the engine code, this PR changes nothing other than adding an extra C function on top of memory management code (that I can eliminate if we want the extra complexity). For testing code, there is no such thing as hypothetical bugs because 100% of the testing code is tested. |
|
This PR looks like what I remember our first attempt being. IIRC the issue here is something like: because it doesn't have the proper annotations, these functions behave differently when compiled in ARC vs non-ARC translation units. That means that if two TUs, one with ARC and one without, both include this header (which is going to be the case), you have two different implementations of the same function, and that violates the ODR. ODR violations result in undefined behavior. I saw fun things like ARC TUs getting the non-ARC codepath, and the other way around.
There is such a thing as undefined behavior though, and you can't test your away around it because it's undefined. Is there a reason you're not just taking the working implementation from Chromium? |
|
Thanks Stuart for the response.
These functions are in a .mm file that is compiled into libFlutter and only compiled with non-ARC so there is no opportunity for them to behave differently. Ignore for a second that I'm doing this PR to support my ARC test. If you just look at the change you see this is the same behavior as the previous header when it is only included in non-ARC code.
We can see the non-ARC code will compile the same. With respect to ARC code, the only ARC code is test code which means we have 100% test coverage. If the tests pass there is no bug.
Undefined behavior isn't a problem for tests since everything is asserted to work a certain way. It is only a problem if the undefined behavior acts inconsistently across the same compiler and platform. There is no real way for me to prove that a compiler doesn't have undefined behavior, it is a risk we live with with all compiled code. I shouldn't have to prove 100% of all possible code creates expected results, especially when dealing with a situation where 100% of actual usage is known.
I noticed after the fact that they just threw an Here's our options:
edit: cc @stuartmorgan |
I don't see any reason to believe that the UB from an ODR violation wouldn't cause hard-to-find flaky test issues.
There is a huge difference between "theoretically, all code could potentially have undefined behavior" and "this code is forked from code where I and some other people tried to solve exactly the problem you are trying to solve (i.e., transitive header includes of scoped_nsobject usage) and my recollection is that the approach you are trying is the one that gave us UB".
That's a recent change, which apparently happened after they completed a full ARC transition and thus no longer needed to solve this problem. https://codereview.chromium.org/1855483004 is where they first made it work.
Which like 1 address this across the board, but unlike 1 has been used extensively in production code, thus we have a high degree of confidence that it doesn't have ODR violations. |
|
@stuartmorgan thanks for the link. I read through their implementation, there is 2 things they are doing different than this PR. They are using extra annotations I can make those 2 changes, or I can bring their code over. Their code is a bit messy and spread out across multiple files. I'd prefer to make those 2 changes but if you feel strongly about it I can try to bring over their files verbatim. |
|
@stuartmorgan There is a lot of baggage bringing over chromes code verbatim. I moved over the difference I saw, please review an let me know if I missed something. |
|
There is one more thing chrome does that we don't, |
|
Added |
0bbd485 to
0eda998
Compare
| static NST InvalidValue() { return nil; } | ||
| }; | ||
|
|
||
| #if __has_feature(objc_arc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused by this ifdef; why do you want different ifdef paths on the implementations? That seems like it significantly increases the chances of an accidental ODR violation. The same declarations and implementation of the wrappers should work for both. That was the whole point of the structure of the Chromium change.
In fact, it looks to me like this guarantees ODR violations, since you'll generate two copies of definitions of this template class (for any given NST), with different implementations. Am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ifdef's removed
|
I talked to @stuartmorgan offline. The criteria for which he'd feel comfortable making any change are undefined since a risk is believed to exist but he has no way to prove a change presents a problem or not because the problem is not fully understood. Anything short of a verbatim copy from chromium code and its dependencies presents an unacceptable risk. The one thing he is sure about is that the definitions should be the same across TU's so we should avoid ifdefs and the one thing I'm sure about is that we should have the annotations like Given what has been discussed I think there should be a unit test compiled and run with ARC and one for manual reference counting to make sure we get the annotations correct. That may be moot if stuart doesn't feel comfortable even with that assurance. |
This is not an accurate summary of my position. Given that after several rounds of discussion we still don't seem to have reached a common understanding of the situation, I think the best option is to continue with another reviewer. |
|
@stuartmorgan Ok, since this isn't a priority right now I'll drop this back to a draft, implement the unit tests that run on ARC and non-ARC, maintain the policy of no-ifdefs to avoid your concern about ODR violations and solicit the review from an elder statesman. |
dmaclach
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general the ARC and mem management LGTM.
| // We check for bad uses of scoped_nsobject and NSAutoreleasePool at compile | ||
| // time with a template specialization (see below). | ||
|
|
||
| class scoped_nsprotocol_arc_memory_management { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Been a while since I've done heavy C++, but can this just be a namespace?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, it could be. The reason things are a laid out the way they are is to keep the code somewhat similar to the chrome implementation. I'll give it another style pass once I can verify that the generated machine code is fine.
Well, that's what Rohit and I thought about the first broken version :) Addressing a comment from earlier:
Since that's how we figured out that the version we wrote had UB in the first place, I agree that this should be a requirement for landing any version of this change. |
I didn't actually see all of this background discussion until after I posted my comment. I was busy editing my previous post and then saw that you had already replied.
Seems reasonable. As you mention it does not guarantee that ODR/UB problems don't exist, but as Aaron points out it gives us as good a bar as any of our other code, that is we wrote a test that executed the code paths that we were concerned about in the environments that we were concerned about, and it passed. I'll be interested to see if Mark has any other thoughts. |
|
I had @bdash look into this. He said there may be concerns with an ODR violation. He suspects that we might be able to get to a point where the annotations cause the compilation of this code with ARC and without ARC to be identical. If we could get to that point and show that they were identical there would be no possible ODR violation since whatever version of the machine code the loader grabbed, they would be identical. That gives this PR an avenue forward, I'll give it a look. He also mentioned they use |
|
To be explicit, the ODR concern is that when compiled under ARC vs MRC, the compiler will generate different machine code for methods that deal directly with the Obj-C pointer type (either It's up to the linker which of the many definitions of an inline function is selected, and there's no guarantee it'll select the ARC vs MRC version for any given member function, which may lead to leaks or over-releases if they end up being mixed. The approach I suggested was to ensure that every use of |
|
Ok, I took a look. As the code stands today here is what the disassembly looks like for arc/mrc for the scoped_nsobject constructor: I added the following patch: - explicit scoped_nsobject(NST* object = Memory::InvalidValue())
+ explicit scoped_nsobject(__unsafe_unretained NST* object = Memory::InvalidValue())
: scoped_nsprotocol<NST*>(object) {}The disassembly for the arc version of the constructor after this change is identical. I'll ponder this a bit more. I think just making scoped_nsobject just a wrapper around scoped_nsobject could avoid this problem. |
|
If anyone else wants to play around with it I posted my testbed: https://github.com/gaaclarke/scoped_nsobject_test |
|
Notice that the symbols for instantiations of That means that there can't be an ODR violation for The problem is that the way class scoped_nsobject : public scoped_nsprotocol<NST*>If we can find a way to preserve that type information we wouldn't have a problem. That might mean having users say |
|
Ugh. From the generated code it looks like Clang is allocating a stack slot to hold the object, zeroing it, then using You're right that if the pointer type, rather than the class type, is used as the template parameter then you get different mangling which may be sufficient to avoid ODR problems. Relying on that would require changing the way these smart pointers are used, but it seems like it may be a safer approach than trying to coax Clang into generating exactly what you're after. |
74826de to
6e52cd2
Compare
maintains special typenames for classes being maintained by ARC. This avoids an ODR violation where ARC / MRC machine code is randomly grabbed incorrectly for 2 equal symbols. Now ARC / MRC versions of templates have unique symbols.
6e52cd2 to
4d9d980
Compare
|
It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie on the #hackers channel in Chat. Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
Ok, I pushed a commit where switched |
| static void Release(id object) { [object release]; } | ||
| static id InvalidValue() { return nil; } | ||
| }; | ||
| #endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this introduces another ODR violation. You again have symbols with identical names where the definitions are different in different translation units (ARC vs non-ARC). In this case, the violation is even clearer since the definitions consist of different tokens, rather than the earlier iteration where the same tokens led to different machine code due to ARC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What symbols do you believe are the same between the ARC and non-ARC TUs? If fml::scoped_nsobject<Foobar*> foo; is compiled with ARC we should have a fml::scoped_nsobject<Foober* __strong>::scoped_nsobject() symbol and if it is compiled without ARC it will be a fml::scoped_nsobject<Foobar*>::scoped_nsobject() symbol.
I don't think there is a way, and it definitely isn't easy, to create a fml::scoped_nsobject<Foobar*>::scoped_nsobject(); when compiling with ARC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The symbols in the scoped_nsprotocol_memory_management class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though I suppose they may also end up with different mangling due to taking parameters / returning values that are Obj-C types? I'm not clear if the difference in mangling is specific to template type parameters, or if it extends to parameters and return types as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh good catch, thanks! I addressed it by making scoped_nsprotocol_memory_management templated around the NST.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(at the cost of a slightly larger binary)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What symbols do you believe are the same between the ARC and non-ARC TUs? If
fml::scoped_nsobject<Foobar*> foo;is compiled with ARC we should have afml::scoped_nsobject<Foober* __strong>::scoped_nsobject()symbol and if it is compiled without ARC it will be afml::scoped_nsobject<Foobar*>::scoped_nsobject()symbol.
If scoped_nsobject evaluates to different classes in ARC and non-ARC, then you're just pushing the ODR violation up a level; any C++ class with a scoped_nsobject ivar that's included from both ARC and non-ARC code (which is the case this whole effort is intended to allow, so not an edge case) is now violating the ODR, because there are two different versions of that class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stuartmorgan I think you're mistaken. If you have:
class Foo {
public:
Foo();
private:
fml::scoped_nsobject<NSArray*> array_;
};You will only get 1 version of Foo::Foo(), it will be the one that gets compiled from foo.mm. It will reference fml::scoped_nsobject<NSArray*>::scoped_nsobject() or fml::scoped_nsobject<NSArray* __strong>::scoped_nsobject() depending on if it was compiled with ARC or not. Were you thinking of a different setup?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stuart's correct. Consider what happens if an inline function in a header file interacts with a scoped_nsobject<NSArray *>. It'll emit references to different symbols depending on whether it is imported into an ARC or MRC translation unit. That is an ODR violation since the same function now has different definitions in different translation units.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh yea. In the above example ~Foo() would be problematic, right?
One possible way forward would be to annotate the code correctly and prove that the machine code is equivalent (if not equal) with unit tests. That sounds like a fair bit of work and has a risk of not panning out.
Another would be do so something similar to what @bdash mentioned where we use void* or CFTypeRef to bypass ARC's code generation to make the machine code equivalent.
I'm loath to take any more of your time, I thought I could hammer this out quickly. We can live without ARC, it's just a shame to live without it. I've spent a lot of time fixing memory bugs in our objc code and we have a whole lot more to write while we catch up with unit tests.
|
I'm closing this PR. I think the best approach would be to delete scoped_nsobjects, which is a better alternative than the more complicated task of migrating them to the pimpl pattern and enforcing they aren't used in headers. |
All of our unit tests are written with ARC. In order for us to write those tests, non-ARC code must live in the binary, not in the headers to avoid
clang++ -fobjc-arcfrom attempting to compile them when including the headers of the classes it wants to test.Related PR: #18281