Recent

Author Topic: [SOLVED] Statically linking multiple packages that use the C runtime  (Read 2477 times)

Khrys

  • Jr. Member
  • **
  • Posts: 82
System information:
OS (Architecture): Windows 10 (AMD64/x86-64)
Lazarus Version: 2.0.12
FPC Version: 3.2.0
C Compiler: GCC 10.3.0 (TDM-GCC-64)


Hello everyone,

this is what I'm trying to achieve:
  • Create an IDE-installable Lazarus package that statically links PCRE2; usable on Windows
  • Create an IDE-installable Lazarus package that statically links SQLite3; usable on Windows
  • Have the ability to independently use either one or both of these packages at once (without relying on glue/dummy packages)
In isolation I've managed to reach the first two goals: I've created two packages that bundle C source code, which (given a working installation of GCC) compile a static version of the respective library via a custom makefile. The Windows targets require some extra hand-holding; notably, instead of a simple  {$linklib c}  they require the GNU-style static libraries  libgcc.alibmsvcrt.a  and  libkernel32.a. These can be located automatically via GCC by using eg.  "gcc --print-file-name=libgcc.a", so besides conditionally  {$linklib}ing these, the makefile is able to handle this. The end result is a source-only package that compiles and links just fine on both Linux and Windows, plus it can be installed in (that is, linked into) Lazarus.

Now, this is where the complications begin (all on Windows, because of course it had to be Windows):
On x86-64, while linking PCRE2 I was initially hit with internal compiler error 200603061 ("Only debug sections are allowed to have relocs pointing to unused sections") which stumped me for a while. The first thing I tried was switching to the external linker (-Xe), which solved this issue (and would solve most other issues I'm going to bring up), but then I found out that Lazarus itself won't link with -Xe, making that option a dealbreaker since non-installable packages propagate that limitation to any downstream components. In other words, it seems that for package interoperability only FPC's internal linker may be used.

(I eventually worked around the internal error by stumbling upon GCC's  mcmodel=small  option, which gets rid of the relocations that the internal linker can't handle. So for anyone reading this in the future: If you have problems trying to use FPC's internal linker to link object files stemming from multiple co-dependent translation units, try using  -mcmodel=small)

Ok, so with two individual working packages and knowing that  -Xe  is a no-no, I tried using both packages in the same project, and hit the roadblock I'm still stuck at today: No more than one such package will successfully link at a time. By "such package" I mean a package that bundles a C library like so (also setting  -Fl[...] etc.):

Code: Pascal  [Select][+][-]
  1. unit Something_Interface;
  2.  
  3. implementation
  4.  
  5. procedure something_foo(); cdecl; external;
  6.  
  7. implementation
  8.  
  9. {$linklib something} // Contains definition of something_foo
  10. {$if defined(WINDOWS)}
  11.   {$linklib gcc}
  12.   {$linklib msvcrt}
  13.   {$linklib kernel32}
  14. {$else}
  15.   {$linklib c}
  16. {$endif}
  17.  
  18. end.

The first issue I stumbled upon when I tried using both packages at once was that certain symbols from libgcc, libmsvcrt and libkernel32 remained undefined (_strncmp__divdi3_imp_CloseHandle@4 etc.) remained undefined. Looking at the extended debug output (-va), I found out why:

Code: [Select]
Debug: [2.001] Opening library [...]/libpcre2.a
Debug: [2.002] Opening library [...]/libgcc.a
Debug: [2.003] Opening library [...]/libmsvcrt.a
Debug: [2.004] Opening library [...]/libkernel32.a
Debug: [2.005] Opening library [...]/libsqlite3.a

A link order issue! After libpcre2 resolves all the symbols it needs, the remaining symbols are discarded. Unfortunately, this happends before libsqlite3 can resolve any additional symbols it needs from those libraries. (Please correct me if I'm wrong, but I think that is what's happening)
To me it seemed like FPC didn't bother opening the same library more than once, so I tried to trick the compiler by adding no-op modifications to the  {$linklib}  file names like  {$linklib .//./libgcc.a}  instead of  {$linklib libgcc.a}  - which actually worked, but only when doing a clean rebuild (to be precise, when no  .ppu  was present for the header translation units). So I tried using the advanced linker options (-XLO-XLA-XLD; the first two seemed promising), but didn't notice any effects, so I dug into the FPC source (release_3_2_0) and found that  -XLO  and  -XLA  are only handled on Linux, Android and BSD (no other target calls  ExpandAndApplyOrder).
I made a final attempt with the  -k  option, but as I soon found out, it unfortunately only has an effect on Windows targets (uses  ParaLinkOptions) when using the external linker... which I cannot use (as detailed above).

So now my only hope left is help from a true expert - let's hope one stumbles upon this post...  :-[
« Last Edit: April 30, 2024, 02:17:45 pm by Khrys »

Thaddy

  • Hero Member
  • *****
  • Posts: 15555
  • Censorship about opinions does not belong here.
Re: Statically linking multiple packages that use the C runtime
« Reply #1 on: April 29, 2024, 04:41:39 pm »
You should be able to combine {$if defined xxx}with {$if declared(xxx.feature)} to exclude duplicate linking. Just a though.
If I smell bad code it usually is bad code and that includes my own code.


Khrys

  • Jr. Member
  • **
  • Posts: 82
Re: Statically linking multiple packages that use the C runtime
« Reply #3 on: April 30, 2024, 02:16:56 pm »
Hi everyone,

thanks for the suggestions. After dissecting the compiler sources some more, I finally found a workaround: mark the libraries that are supposed to come last as  shared  while marking all other libraries as  static:

Code: Pascal  [Select][+][-]
  1. unit Something_Interface;
  2.  
  3. implementation
  4.  
  5. procedure something_foo(); cdecl; external;
  6.  
  7. implementation
  8.  
  9. {$linklib something, static}  // <------| Link first -> static
  10. {$if defined(WINDOWS)}
  11.   {$linklib gcc, shared}      // <------|
  12.   {$linklib msvcrt, shared}   // <------| Link last -> shared
  13.   {$linklib kernel32, shared} // <------|
  14. {$else}
  15.   {$linklib c}
  16. {$endif}
  17.  
  18. end.

Turns out that the internal linker (on Windows) makes no distinction between static and shared libraries apart from deferring file name resolution for the latter, but - and this is crucial - it first goes through all static libraries before moving on to shared libraries (release_3_2_0: link.pas[974..1000]; AddSharedAsStatic = True  on Windows):

Code: Pascal  [Select][+][-]
  1. procedure TInternalLinker.ScriptAddSourceStatements(AddSharedAsStatic:boolean);
  2.   var
  3.     s,s2: TCmdStr;
  4.   begin
  5.     while not ObjectFiles.Empty do
  6.       begin
  7.         s:=ObjectFiles.GetFirst;
  8.         if s<>'' then
  9.           LinkScript.Concat('READOBJECT '+MaybeQuoted(s));
  10.       end;
  11.     while not StaticLibFiles.Empty do
  12.       begin
  13.         s:=StaticLibFiles.GetFirst;
  14.         if s<>'' then
  15.           LinkScript.Concat('READSTATICLIBRARY '+MaybeQuoted(s));
  16.       end;
  17.     if not AddSharedAsStatic then
  18.       exit;
  19.     while not SharedLibFiles.Empty do
  20.       begin
  21.         S:=SharedLibFiles.GetFirst;
  22.         if FindLibraryFile(s,target_info.staticClibprefix,target_info.staticClibext,s2) then
  23.           LinkScript.Concat('READSTATICLIBRARY '+MaybeQuoted(s2))
  24.         else
  25.           Comment(V_Error,'Import library not found for '+S);
  26.       end;
  27.   end;

This effectively allows manually controlling the link order of 2 library groups, which is enough for my intents and purposes. It's not an elegant solution since it relies on an implementation detail of the compiler that may or may not change in the future, but oh well...

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 11732
  • FPC developer.
Re: [SOLVED] Statically linking multiple packages that use the C runtime
« Reply #4 on: April 30, 2024, 03:26:57 pm »
When building a mingw based IDE I had similar problems back then in 2010 too.

I solved the problems then, by including the same static libraries multiple times with $linklib, once before and once after the other library with dependencies.

Thaddy

  • Hero Member
  • *****
  • Posts: 15555
  • Censorship about opinions does not belong here.
The order in which you link is very strict.
I didn't know about Marco's work-around, though.

A good example is the static SQLite linking in mORMot.
If I smell bad code it usually is bad code and that includes my own code.

Khrys

  • Jr. Member
  • **
  • Posts: 82
From what I've seen elsewhere online, Marco's solution of specifying libraries multiple times actually seems to be the recommended way to do it. GCC even has  -(  and  -)  options that allow groups of archives with circular dependencies to be searched repeatedly until all undefined symbols are resolved (i.e.  libalpha  depends on  libbeta, but  libbeta  is weirdly intertwined with  libgamma  and  libdelta):
Code: Text  [Select][+][-]
  1. gcc -o prog ... -lalpha -( -lbeta -lgamma -ldelta -)

I like to think of references to dependencies/external symbols as shopping lists and the dependencies/libraries themselves as stores: first get a shopping list and only then go to the store, because otherwise you'll forget a bunch of stuff...

But in any case, my problem here was really caused by the way Lazarus packages work in combination with my stubbornness on how I want my packages to interact; I could've just put all  {$linklib}  directives in a single file to sort out the link order myself, but then the packages wouldn't be as self-contained and plug-and-play as I'd want them to be.

(None of my problems here popped up in the Linux target though; everything just works there... I guess it's time to blame Windows again :P)

 

TinyPortal © 2005-2018