Delphi Calling C Code: Static Linking or Dynamic Linking? 90% of People Choose Wrong!

The main content of this article is as follows:

  • Static Linking: Convenient but Dangerous
  • Dynamic Linking (DLL): Clear Boundaries, Stable as a Rock
  • Practical Examples
  • How to Choose?
  • Why Was “Static Linking C Code” Popular?

Connecting Delphi and C: Practical Experience with C Object File Linking (Part 1)Connecting Delphi and C: Practical Experience with C Object File Linking (Part 2)Connecting Delphi and C: Practical Experience with C Object File Linking (Part 3)

🔥 Ultimate Advice:

Unless you are very sure the environment is controllable (e.g., only Win32 + self-developed C library + no macro dependencies), always use the DLL solution.

The little deployment convenience saved does not outweigh the time cost of troubleshooting ABI crashes later.

In Delphi development, we often need to reuse mature C/C++ libraries—such as regular expression engines, encryption algorithms, image processing modules, etc. But the question arises: Should we use static linking (.obj/.o) or dynamic linking (DLL)?

Many people’s first reaction is: “Just link the .obj file directly, it’s convenient and doesn’t require bringing a DLL!” But the truth is: Static linking seems simple, but it actually hides traps; dynamic linking is the “correct way” for cross-language integration..

Today, we will clarify the differences between the two based on key points such as RAD Studio version features, 64-bit compatibility, runtime dependencies, and provide clear recommendations.

Static Linking: Convenient but Dangerous

1. Poor 64-bit Compatibility

  • C++Builder 64-bit uses Clang/LLVM, generating ELF format .o files;
  • Although the Delphi 64-bit compiler (DCC64) has supported this format since XE4, the ABI is completely incompatible:
    • Different symbol decoration rules;
    • Conflicts in exception models (Delphi uses SEH, C++ uses C++ exceptions);
    • Runtime libraries (RTL) do not recognize each other.
  • The result is that even if the linking is successful, it is prone to runtime crashes.

📌 The official documentation clearly states: Delphi and C++Builder cannot directly link each other’s object files..

2. Circular Dependencies Are Hard to Resolve

When you need to link multiple object files, you may encounter situations like this: for example, <span>one.obj</span> calls a function from <span>seven.obj</span>. Since the Delphi compiler is essentially one-pass compilation, if you write in your code:

{$LINK 'one.obj'}{$LINK 'seven.obj'}

Then the linker will find that it references an unresolved symbol (from <span>seven.obj</span>) when processing <span>one.obj</span>, resulting in an “unresolved external symbol” (unsatisfied externals) error.

One solution is to adjust the order of <span>{$LINK}</span>, placing the dependent files first:

{$LINK 'seven.obj'}  // Link the file providing the symbol first{$LINK 'one.obj'}    // Then link the file using the symbol

But the problem arises: if <span>seven.obj</span> also references a function or variable from <span>one.obj</span>, this creates a circular dependency. In this case, no matter how you adjust the order, the problem cannot be resolved.

In such cases, you need to make <span>{$LINK}</span> directive before to declare the mutually referenced functions (similar to forward declarations in Pascal):

function _getLocaleName; external;   // Defined in one.obj, used by seven.objfunction _convertCase; external;     // Defined in seven.obj, used by one.obj{$LINK 'one.obj'}{$LINK 'seven.obj'}

This tells the compiler: “These functions, although not yet defined, will be found in the linked object files later,” thus avoiding linking errors.

3. Missing Runtime Functions

C code relies on <span>malloc</span>, <span>strlen</span>, and other standard library functions, which Delphi must implement manually or import from <span>msvcrt.dll</span>. Worse still—many “functions” are actually macros (like <span>getchar()</span>), which cannot be linked at all!

Rather than manually implementing all C runtime functions (like <span>strlen</span>, <span>strchr</span>, etc.), it is better to directly call the Microsoft Visual C++ Runtime Library (<span>msvcrt.dll</span>). Some parts of msvcrt.dll have been replaced by the System.Win.Crtl.pas unit introduced in Delphi XE2. This unit has encapsulated some runtime functions exposed in <span>msvcrt.dll</span>. Notably, these functions in the unit provide both underscored and non-underscored declaration forms.

âť— Macros Are Not Functions!

Some “functions” in <span>msvcrt.dll</span><span> are not real functions, but macros</span><span>!</span><span> These macros directly access internal structures (like the </span><code><span>FILE</span> buffer), and these structures are not consistent across different compilers (BCB, Delphi, MSVC), and may even be invisible.

Example:

<span><span>getchar()</span></span> and <span><span>putchar()</span></span>

For example, <span>getchar()</span><span> is defined as a macro in </span><code><span>stdio.h</span><span>:</span>

#define getchar() (--stdin->_cnt >= 0 ? (unsigned char)*stdin->_ptr++ : _filbuf(stdin))

It relies on <span>stdin</span><span> (which is also a macro pointing to </span><code><span>_streams[0]</span><span>) and its internal fields (like </span><code><span>_cnt</span><span>, </span><code><span>_ptr</span><span>). </span>

This means: No matter how you declare <span>getchar</span><span>, your function will not be called</span>—because the C compiler expands it during the preprocessing stage.

To solve this problem, you have to:

  1. Declare the <span>__streams</span> array in Delphi;
  2. Initialize its <span>level</span> (or <span>_cnt</span>) field to a negative value to force it to go through <span>_fgetc</span> path;
  3. Implement <span>_fgetc</span> yourself, checking if the incoming stream is <span>@__streams[0]</span><span> (i.e., standard input);</span>
  • If it is, call Delphi’s <span>Read</span>;
  • Otherwise, actually call <span>msvcrt.dll</span><span>’s </span><code><span>_fgetc</span><span>.</span>

⚠️ But note: <span>msvcrt.dll</span><span> has its own </span><code><span>FILE</span> structure implementation, which is completely different from the layout of BCB/Delphi. So even if you initialize <span>__streams</span><span>, </span><code><span>msvcrt.dll</span><span> will not read it—this is the root of the problem.</span>

Similarly, <span>putchar()</span><span> is also a macro that may modify the buffer state, causing the same problem.</span>

🤯 Conclusion: Macros are the source of evil! (Macros are evil evil evil!)

âť— <span><span>fgetpos()</span></span> and <span><span>fsetpos()</span></span><span><span> Pitfalls</span></span>

These two functions in <span>msvcrt.dll</span><span> will write to the </span><code><span>pos</span><span> parameter</span><strong><span> more than 4 bytes of data</span></strong><span>.</span><span> However, in BCB's </span><code><span>stdio.h</span><span>, </span><code><span>fpos_t</span> is simply defined as <span>long</span><span> (i.e., 4 bytes).</span><span> When you call </span><code><span>_fgetpos()</span><span> with BCB's declaration, it will cause</span><strong><span> a buffer overflow</span></strong><span>, leading to an access violation.</span>

The solution is: to bypass them and use <span>_fseek()</span><span> and </span><code><span>_ftell()</span><span> to implement position saving/restoration.</span>

📌 In principle, <span>fpos_t</span><span> should be an "opaque type" and should only be manipulated through pointers.</span><span> But BCB has concretized it as </span><code><span>long</span><span>, limiting its size. And </span><code><span>msvcrt.dll</span><span> may write 8 bytes or more—this is the risk of cross-compiler integration.</span>

4. Memory Management Chaos

If C code allocates memory using <span>malloc</span><span> and Delphi frees it using </span><code><span>FreeMem</span><span>, it can easily lead to heap corruption.</span>

Linking C object files to Delphi units is actually quite simple.

The biggest advantage of this method is: No additional DLL required, and the program can be packaged as a single executable file, making deployment easier.

Dynamic Linking (DLL): Clear Boundaries, Stable as a Rock

1. Fully Cross-Compiler Compatible

  • As long as you export a pure C interface (<span>extern "C"</span> + <span>__declspec(dllexport)</span>), any language can call it.
  • Not restricted by ABI, exception models, or compiler backends.

2. Inherently Isolated Runtime

  • C++ code inside the DLL uses its own CRT (C Runtime);
  • Delphi is only responsible for passing parameters and receiving results, zero coupling.

3. Easy to Maintain and Update

  • Updating algorithms only requires replacing the DLL, no need to recompile the main program;
  • Supports hot swapping and plugin architecture.

Practical Example

C++Builder Side (Generating DLL)

// regex_wrapper.cppextern "C" __declspec(dllexport)void* regcomp(const char* pattern) {    return ::regcomp(pattern);}extern "C" __declspec(dllexport)int regexec(void* prog, const char* str) {    return ::regexec((regexp*)prog, str);}

Delphi Side (Calling DLL)

function regcomp(pattern: PAnsiChar): Pointer; cdecl; external 'regex64.dll';function regexec(prog: Pointer; str: PAnsiChar): LongBool; cdecl; external 'regex64.dll';

How to Choose?

Scenario Recommended Method
Win32 Tools, No Complex Dependencies Static Linking (.obj) Can Be Tried
Win64 Projects ❌ Static Linking is Basically Not Feasible → Must Use DLL
Involves STL, Exceptions, Complex Types Can Only Use DLL
Requires Long-term Maintenance or Team Collaboration Strongly Recommend DLL
Pursuing Single File Deployment Can Consider Packing DLL into Resources, Releasing at Runtime (like Inno Setup’s <span>solid</span> mode)

Why Was “Static Linking C Code” Popular?

In the era of Delphi 7 / BCB6, Win32 was mainstream, and the compilers were from the same source (Borland toolchain),<span>.obj</span> formats were unified, and static linking was indeed feasible. However, with the popularity of 64-bit, the introduction of Clang, and the strengthening of Windows security mechanisms, this “glue-style integration” has become history.

Modern software engineering emphasizes clear boundaries and separation of responsibilities—DLL is the best embodiment of this idea.

There is no silver bullet in technology, but there are best practices.

In mixed programming between Delphi and C/C++, dynamic linking is not a fallback option, but the correct path for the future.

Next time you want to <span>$LINK 'xxx.obj'</span><span>, ask yourself:</span>

“Am I saving time, or laying a mine?”

Leave a Comment