1

After #importing a .tlb file, I tried to use one of the generated _com_ptr_t objects in a function template. This works fine if the functions I use are also used outside of a function template. However if it's only used in a function template, I get a link error.

Can anyone explain why this happens? I'm guessing it's to do with template instantiation but I'm not sure how the #import mechanism works. Is it the expected behaviour?

Here's a minimal example which includes all combinations according to the value of COMPILE_OPTION.

  1. Define only the regular function that uses the object (causing it to be instantiated)
  2. Define only the function template (which doesn't cause the used function to be instantiated) - This is the problem case
  3. Define both, this shows that if the object is used in a regular function the template option works as expected (and not as in case #2)

Code:

#import <mshtml.tlb>

#if COMPILE_OPTION & 1
int foo(MSHTML::IHTMLElementPtr elem) {
    return elem->tagName.length();
}
#endif

#if COMPILE_OPTION & 2
template <class T>
int bar(T elem) {
    return elem->tagName.length();
}
#endif

int main() {
#if COMPILE_OPTION & 2
    bar(MSHTML::IHTMLElementPtr());
#else
    foo(MSHTML::IHTMLElementPtr());
#endif
}

Now the following compile successfully:

cl Source.cpp /DCOMPILE_OPTION=1
cl Source.cpp /DCOMPILE_OPTION=3

But this one:

cl Source.cpp /DCOMPILE_OPTION=2

Produces the following (relevant error in bold):

Source.obj : error LNK2019: unresolved external symbol "public: class _bstr_t __thiscall MSHTML::IHTMLElement::GettagName(void)" (?GettagName@IHTMLElement@MSHTML@@QAE?AV_bstr_t@@XZ) referenced in function "int __cdecl bar<class _com_ptr_t<class _com_IIID<struct MSHTML::IHTMLElement,&struct __s_GUID const _GUID_3050f1ff_98b5_11cf_bb82_00aa00bdce0b> > >(class _com_ptr_t<class _com_IIID<struct MSHTML::IHTMLElement,&struct __s_GUID const _GUID_3050f1ff_98b5_11cf_bb82_00aa00bdce0b> >)" (??$bar@V?$_com_ptr_t@V?$_com_IIID@UIHTMLElement@MSHTML@@$1?_GUID_3050f1ff_98b5_11cf_bb82_00aa00bdce0b@@3U__s_GUID@@B@@@@@@YAHV?$_com_ptr_t@V?$_com_IIID@UIHTMLElement@MSHTML@@$1?_GUID_3050f1ff_98b5_11cf_bb82_00aa00bdce0b@@3U__s_GUID@@B@@@@@Z)

Source.exe : fatal error LNK1120: 1 unresolved externals

Notes:

  1. A workaround is to use the raw COM methods
  2. This is obviously a simplified version, the reason I'm using a template is that I'm using a library that has several types with common functionality that don't share a common base interface.
Motti
  • 99,411
  • 44
  • 178
  • 249
  • Also it’s not a template ‘class’. Usually see template – DS_London Apr 27 '21 at 06:40
  • 2
    @DS_London there's no difference between using `class` or `typename`: https://stackoverflow.com/questions/2023977/difference-of-keywords-typename-and-class-in-templates#:~:text=There%20is%20no%20semantic%20difference,referring%20to%20a%20dependent%20type. – Alan Birtles Apr 27 '21 at 06:44
  • @AlanBirtles thanks ... did wonder that, just all the examples I see on cppreference.com seem to differentiate. – DS_London Apr 27 '21 at 06:49
  • 1
    @DS_London in templates `class` and `typename` are equivalent. I (and others) tend to use `class` when it's a user-defined class and `typename` when it may also be a primitive type. – Motti Apr 27 '21 at 07:00
  • 1
    Before C++17, I think you _had_ to use `class` in template template parameters though. – Ted Lyngmo Apr 27 '21 at 07:05
  • @TedLyngmo that is not correct, you could use `typename` back in C++98 – Motti Apr 27 '21 at 07:06
  • @Motti [cppreference](https://en.cppreference.com/w/cpp/language/template_parameters) says "_Unlike type template parameter declaration, template template parameter declaration can only use the keyword `class` and not `typename.` (until C++17)_" [godbolt agrees](https://godbolt.org/z/xsEh31jbv) – Ted Lyngmo Apr 27 '21 at 07:11
  • 1
    @TedLyngmo my bad, I missed the double `template` in `template template`... – Motti Apr 27 '21 at 07:20
  • I replicated the error in MSVC 19 (which also generates a slew of 'forward declaration of enum is nonstandard' errors). The linker error shows that it cannot find the implementation needed for elem->tagName.length(). If I remove all the pre-proc #defines etc, it compiles. If I remove all references to foo(), but put a call to MSHTML::IHTMLElementPtr->tagName.length() in main(), it compiles. It seems without a non-templated call, the compiler is not generating the implementation: which somewhat defeats the point of the template in the first place. – DS_London Apr 27 '21 at 09:27
  • NB. Compiles, but won't run as the default IHTMLElementPtr constructor contains a NULL pointer. – DS_London Apr 27 '21 at 10:18
  • @DS_London thanks for going to all the effort to reproduce. This was exactly what I was trying to explain by saying "This works fine if the functions I use are also used outside of a function template. However if it's only used in a function template, I get a link error". Do you think I should clarify the question? – Motti Apr 27 '21 at 11:09
  • @Motti No, I think your question is fine ... I just didn't read it properly! Clearly the compiler thinks it doesn't need to generate the implementation, but I'm afraid I don't know why. – DS_London Apr 27 '21 at 11:56
  • @Motti ... I don't know if this is a clue, but it seems like the linker is not seeing the implementations in the mshtml.tli file (which is included by mshtml.tlh). If I use the #import no_implementation option (which disables the tli file inclusion) and copy the GettagName() implementation into my code, it compiles. That is, I am explicitly giving the compiler the implementation that the linker originally said was missing. – DS_London Apr 27 '21 at 17:20
  • I found this : https://www.betaarchive.com/wiki/index.php/Microsoft_KB_Archive/832688 .Which talks about add the 'no_function_mapping' tag to the #import directive. If I do that, the code compiles and links. – DS_London Apr 27 '21 at 17:51

1 Answers1

1

As per the comments, I unearthed this archived KB Article which talks about the #pragma implementation_key compiler directive, which is used to improve performance when a type library has a lot of methods (over 1000 it seems), which mshtml.tlb does. This directive appears in the generated mshtml.tlh and .tli files. The article also talks about 'side-effects' which is perhaps what is happening here.

If you add the (undocumented!) 'no_function_mapping' modifier to the #import:

#import <mshtml.tlb> no_function_mapping

these #pragmas are removed and the code compiles and links.

[EDIT] A bit more explanation / guesswork. It seems MS have introduced the implementation_key pragma to try to improve the performance (somehow) of importing/imported type libraries: in cases where the caller might only use 2 functions out of 10,000's.

This is the section of the generated mshtml.tlh header that defines all the individual methods of the type library, all 51,468 of them, surrounded by the start_map_region and stop_map_region pragmas. The failing method GettagName() is number 190.

#pragma start_map_region("your_project_path\Debug\mshtml.tli")
__declspec(implementation_key(1)) void IHTMLStyle::PutfontFamily ( _bstr_t p );
__declspec(implementation_key(2)) _bstr_t IHTMLStyle::GetfontFamily ( );

//And on and on, down to the OP's failing method:
__declspec(implementation_key(190)) _bstr_t IHTMLElement::GettagName ( );

//and on and on, down to:
__declspec(implementation_key(51466)) ISVGElementInstancePtr ISVGUseElement::GetanimatedInstanceRoot ( );
__declspec(implementation_key(51467)) long ISVGElementInstanceList::Getlength ( );
__declspec(implementation_key(51468)) ISVGElementInstancePtr ISVGElementInstanceList::item ( long index );
#pragma stop_map_region

And here is the corresponding part in the mshtml.tli file which is #include'd by the mshtml.tlh header:

#pragma implementation_key(190)
inline _bstr_t MSHTML::IHTMLElement::GettagName ( ) {
    BSTR _result = 0;
    HRESULT _hr = get_tagName(&_result);
    if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
    return _bstr_t(_result, false);
}

The KB article refers to side effects of this mapping process. My guess is that when working with this pragmas, the MS compiler can detect explicit calls, eg:

MSHTML::IHTMLElementPtr elem();
elem->tagName.length();

but doesn't recognize the indirect use in the OP's template function.

The no_function_mapping qualifier to the #import statement tells the code generator not to add in the start_map_region, stop_map_region, and implementation_key pragmas, and you get 'regular' code, which recognizes the template substitution.

If the OP had chosen a smaller type library for their test, then this 'feature' may not have been apparent, as the implementation_key pragma might not have been used.

My minimal tests do not show any size change in the compiled executable, with the no_function_mapping modifier.

What the performance improvement might be is unclear. It might be a smaller memory footprint at run-time, as the dual interface doesn't have to keep a track of the dispId's that won't be called. My understanding is a bit vague, but using IDispatch::Invoke() requires determining which dispId will be invoked for a given function name, using some kind of map created after calling IDispatch::GetIdsOfNames(). This map would get pretty large for 50k+ methods, so perhaps the implementation_key approach is a way to reduce the size of the map (and hence the lookup time) to contain only those methods that will actually be used?

DS_London
  • 852
  • 1
  • 12
  • I'm not sure how the KB relates to this issue but it does indeed fix the problem, good catch! – Motti Apr 28 '21 at 07:43
  • 1
    @Motti I added a bit more of an explanation from my investigations ... though you probably know more that you wanted to now! – DS_London Apr 28 '21 at 10:12