Thanks to both Jim and Herb for the replies. This is a reply to Herb, with another short reply and a short reply to Jim following.
@hsutter
Before I go further, let me sanity-check something: Do we agree that the route of "extending TLB #import" leads to a full ATL + MIDL like approach?
No. ATL + MIDL is a reasonable way to write code, but we are talking about something else. We are talking about not only "extending TLB #import" to cover WinRT modules, but also about doing the reverse of #import, that is, parsing regular C++ code and generating wrapper C++ code in order to expose regular code to WinRT.
Here is an example:
// ISO C++
struct I1 : public IInspectable {
virtual void F() = 0;
virtual std::wstring G(int) = 0;
};
struct I2 : public I1 {
virtual winrt_ptr<I1> H() = 0;
};
class C : public I2 {
public:
C();
...
};
The developer would write the above code, use class C in a regular way in the module, and direct the compiler to expose C for consumption by WinRT. There are many different ways to direct the compiler to do this (pragmas, __declspec, predefined bases, separate command line switches, etc), we can talk about pros and cons of each. The compiler would take the above code and generate wrapper C++ code representing C, I2 and I1 for WinRT. I1 would translate into __internal_I1 and would derive from IInspectable. I2 would translate to __internal_I2 and would derive fro IInspectable as well. __internal_I1::G would return a WinRT string. __internal_I2::H would return a refcounted pointer to __internal_I1. There would be a factory for creating a wrapper for C, which would return a refcounted pointer to it. And so on.
We can discuss how to do the translation for delegates, events and everything else. This is a lot to talk about, but from what we see, for every feature a regular C++ mapping of concepts can be done.
Example syntax for events:
// ISO C++
class C ...
{
...
virtual winrt_event<int()>& E() { return _e; }
...
};
...
c.E() += []{ return 2; }; // or 'c->' if c is winrt_ptr<C>
To create an event, you create a member of the type winrt_event<>, then return a reference to that member via an interface function. When you direct the compiler to expose your class for consumption by WinRT, it generates C++ wrapper code that maps winrt_event<> to equivalent constructs in WinRT. If you later import the same component in a different C++ project via #import, the compiler generates C++ wrapper code that maps WinRT constructs back to winrt_event<>. You subscribe to an event using the plus operator or the Subscribe function on winrt_event<>. The latter lets you save a cookie you can use pass to the Unsubscribe function on winrt_event<>.
If you subscribe to an event you expose in the same project, the call goes directly through C++, without passing through WinRT. This is a huge plus for many reasons (eg, optimization), but in case this ever becomes a minus, you can make the call go through WinRT by directing the compiler to export the class that exposes the event and then reimporting the result via #import.
I hope the above is sufficiently clear.
On to the post:
So I'll assume (please correct me if I'm wrong) that we agree that extending the #import model for the consumption side means that the authoring side would employ something like a MIDL language and compiler/toolchain -- so that programmers could specify the additional information that is needed for metadata -- plus a supporting structure of macros, base classes, and/or ATL-like smart pointers. Correct?
No. Smart pointers, base classes and possibly macros - yes. MIDL - no. I am not convinced that MIDL is worse than C++/CX either, but right now let's concentrate on using straight C++ without MIDL.
At the outset of the project, my team was asked by our management to take as primary design requirements that we find a programming model that should: R1. Enable full power and control, including both consumption and authoring. ... R2. Be as simple and elegant as possible for the developer.
Good goals. The approach proposed above achieves both - the wrapper classes allow R2 while being thin enough not to harm R1. It is possible to capture the code generated for wrapper classes and partially rewrite / reuse it to get more R1 if one needs this.
A. What is the minimal code syntax and build steps it would it take to express this in a MIDL-style approach, and across how many source/intermediate files? What's the best we could do in that kind of alternative approach?
For the sake of completeness:
// ISO C++
class Screen {
public:
int DrawLine( Point x, Point y );
};
Screen s;
s.DrawLine( Point(10,40), Point(20,50) );
B. How do you feel about these two goals? 1. Minimize the actual amount of non-portable non-ISO C++ code/tools developers have to write/use to use WinRT. 2. Make the code in the .cpp file itself appear to be using a C++ library, even though it actually is not (it is generated from Windows-specific language/extensions that are just located in a different non-.cpp file).
Ideally, the only non-portable non-ISO C++ code is #import and #pragmas. There is nothing wrong with having generated code. The requirements on the original code from which the code is generated do not have to be artificial. We are trying to add the ABI so that we can cross over to other languages and technologies, fine, let's do this by generating additional code and using that when we are crossing over. When we don't have to cross over, let's just use (instantiate, call, etc) the original code as is, to the maximum extent possible. Should I elaborate or is what I am saying here clear?
I strongly disagree that C++/CX minimizes the actual amount of non-portable non-ISO C++ code developers have to write to use WinRT. Yes, the above example is 6 lines of code in total and you turn it into C++/CX by adding a single 'ref' keyword, but this single 'ref' keyword makes all of the 6 lines non-ISO. 'Screen s;' looks deceptively like ISO, but if 'Screen' is a ref class, it is not ISO, because the ISO standard does not say what the compiler has to do to create an instance of a ref class. Same for the call to 's.DrawLine', the ISO standard does not say what the compiler has to do to call an instance of a ref class.
If we try and evolve your example just a tiny bit, the non-ISO nature of it will become much more apparent. Say, Screen has to be allocated dynamically, then 'Screen s' becomes 'Screen^ s = ref new Screen()'. Whoa, we added a hat and a second ref. That's 3 keywords for 6 lines now. If arguments to DrawLine have to be WinRT objects as well, there are more hats and refs. If we proceed in that direction we will very soon flood a good deal of our code with hats and refs. And we aren't even talking about non-C++ things like events yet, we are just talking about the trivial example you have chosen to start from.
Compare this mess with wrapper classes.
One can view C++ as a framework for using different technologies. We can use very different technologies like, say, ODBC and OLEDB, simultaneously, there are a lot of very diverse technologies like that which we can use, and we can do it all with next to no changes to the language (#import or an analog is all that is required, if that). What C++/CX does is take one such technology (WinRT) and shove it down into the language, breaking internal logic in many places along the way. What the proposed wrapper classes do is keep WinRT outside the language, making it cooperate via a layer of adapter code. It is clear to me that the approach with wrapper classes is a vastly better one.