跪拜 Guibai
← Back to the summary

Linking lwIP as a Library: A Standalone TCP Client on Windows

Running lwIP on Windows (Part 3): Porting lwIP into Your Own Project

In the previous two parts, I kept running lwIP's built-in example_app: modifying its lwipcfg.h, enabling its application switches. This is fine for setting up the environment, but—that's someone else's application, not mine. In a real embedded project, nobody writes their product code inside an example program. In this part, we will follow the standard usage from lwIP's official BUILDING documentation, link lwIP as a library, and write a minimal TCP client.

The companion project for this part is open source: https://gitee.com/buqibuli/lwip-myapp, you can clone the whole repository for reference.

Usage Notes

First, we need to be clear about the tangible benefits of building your own application independently:

It is equally important to be clear that building on Windows and building on an embedded target are fundamentally different:

On Windows (the approach in this part) On Embedded (the approach for a real product)
Integration Method Link lwIP as a library, using official Filelists (full compilation list) Drag source code into your own project, manually perform file-level tailoring (the PPP directory is simply not added)
sys_arch Porting Layer Use the ready-made win32 port, simulated with Windows threads Write your own, or adapt according to the RTOS used—one of the hardest parts of porting
Network Card Driver pcapif borrows a real network card via Npcap Write your own MAC/DMA driver
Resource Constraints Memory and Flash are almost unlimited, IPv6/PPP all enabled runs happily Every KB of Flash/RAM is a budget, requiring maximum resource squeezing

Therefore, the parts you cannot experience on Windows:

But overall, what you can experience is also very valuable: the protocol stack behavior itself (three-way handshake, retransmission, state machine, which is exactly what was captured packet-by-packet in Part 2), the socket API that is completely consistent with embedded systems, and the direction of lwipopts.h tailoring—after turning off a switch, the code segment does get smaller, which you can see at a glance with size, though the absolute value does not represent your target machine.


Part 1: Get the Project, Get It Running

1.1 Prerequisites

git apply ../lwip-myapp/patches/lwip-win32-standalone-fixes.patch

(The patch is 52 lines, only touches two files, you can review it before applying. Readers who followed through from Part 1 only lack the third patch; it's faster to manually change those few lines according to the before-and-after comparison in Section 3.1; for the complete directory layout starting from a clone, see the repository README.)

1.2 Project Structure

Make sure you already have the official lwIP repository.

The new project is placed outside the lwIP repository, at the same level (this is key—the repository remains untouched, my project just references it). The directory layout looks like this:

D:/Projects/                     ← Parent directory is arbitrary, the key is that the two repositories are at the same level
├── lwip/                        ← lwIP official repository (after applying the 1.1 patch, don't touch it anymore)
│   ├── src/                     ← Protocol stack core, compiles to lwipcore
│   ├── contrib/ports/win32/     ← win32 port: pcapif network card driver + sys_arch porting layer
│   └── ...                      ← There are many other directories in the repository, irrelevant to this article, omitted
└── lwip-myapp/                  ← My project (cloned from the companion repository)
    ├── CMakeLists.txt           ← Integration entry point: sets LWIP_DIR/WPDPACK_DIR, includes two Filelists, links lwipcore + win32 port library
    ├── main.c                   ← All application code (159 lines): four initialization steps + one socket TCP client thread
    ├── lwipopts.h               ← lwIP compile-time configuration, copied verbatim from example_app (already included in the repository)
    ├── lwipcfg.h                ← Network card port configuration, a streamlined version I wrote myself (Section 1.3; not committed to the repo, the template is under docs/ in the repository)
    ├── ppp_settings.h           ← Copied from example_app (already included in the repository), not a single character can be missing (why → Section 3.2)
    ├── patches/                 ← The three lwIP patches mentioned in Section 1.1
    └── docs/                    ← Reference documents copied from the lwIP repository: BUILDING, win32 readme, lwipcfg.h.example

(This directory tree is just a reference example; the only actual requirement is: lwip and lwip-myapp are placed in the same parent directory—CMakeLists looks for lwIP according to this layout by default, the default value for LWIP_DIR is ../lwip.)

1.3 lwipcfg.h: Only Three Places Need to Be Changed to Yours

See the repository for the complete file (docs/lwipcfg.h.example has explanations for all configuration items). There are really only three places that need to be changed to values for your own machine:

For detailed configuration instructions, see "Setting up an lwIP Debugging Environment on Windows with MinGW: Methods and Pitfalls"

1.4 Build

cd /d/Projects/lwip-myapp
cmake -S . -B build -G Ninja -DWPDPACK_DIR=D:/npcap-sdk-1.16   # First time configuration, replace the path with your own
cmake --build build
./build/myapp.exe

Expected output: lwIP initialized, local IP is 192.168.0.200connected, sending HTTP request → prints the received HTTP response. See Part 4 for the complete verification checklist.

If you only want the result, this is enough. Below, I'll explain why each file is written this way, and the three pitfalls I encountered during the first build.

Part 2: Why It's Written This Way

2.1 Two "Necessary Understandings"

Necessary Understanding 1: What "porting partial functionality" actually tailors.

Initially, I thought "porting partial functionality" meant picking some .c files from lwIP to compile. After reading BUILDING and the Filelists, I found out that's not the case. There are two levels of means, each finer than the last:

Level Means What It Controls
Library Level Which library targets to link lwipcore (protocol stack core, required), lwipallapps (official apps like http/mqtt, optional), lwipcontribexamples (examples, don't want)
Feature Level lwipopts.h compile-time switches LWIP_IPV6, LWIP_UDP, LWIP_SOCKET... Disabled features simply don't participate in compilation

In a nutshell: It's not picking source files, it's "selecting libraries + toggling switches"!!

Necessary Understanding 2: The minimum pieces needed for a runnable lwIP application.

Tracing the dependency relationships of example_app, four pieces are indispensable:

  1. lwipcore — The protocol stack core (provided by src/Filelists.cmake)
  2. sys_arch porting layer — Thread/semaphore/mailbox abstraction. On Windows, directly use the win32 port's sys_arch.c, no need to write your own.
  3. netif network card driver — The win32 port's pcapif.c (sends/receives real Ethernet frames via Npcap, see Part 1 of this series for the principle).
  4. Two configuration headerslwipopts.h (protocol stack configuration) and lwipcfg.h (network card and other port configurations, pcapif.c will directly #include it).

Items 2 and 3 together form the win32 port library lwipcontribportwindows (provided by contrib/ports/win32/Filelists.cmake). So all we need to write is: one CMakeLists, one main.c, and two configuration headers.

2.2 Key Points for Writing the Three Files

CMakeLists: The skeleton is only four lines, the rest are paths and variables.

include(${LWIP_DIR}/src/Filelists.cmake)                  # Get lwipcore (protocol stack core)
include(${LWIP_CONTRIB_DIR}/ports/win32/Filelists.cmake)  # Get lwipcontribportwindows (sys_arch + pcapif)
add_executable(myapp main.c)
target_link_libraries(myapp lwipcontribportwindows lwipcore)

This approach is specified by the BUILDING documentation.

main.c: Four initialization steps + one client thread. Compared to example_app's test.c (773 lines, mixing PPP/SLIP/dozens of applications all together), the initialization is distilled down to just four steps—first start the protocol stack in main, then bring up the network card within the tcpip thread context:

tcpip_init(myapp_on_tcpip_init, &init_sem);  // In main: start the protocol stack, callback executes in the tcpip thread

// Inside myapp_on_tcpip_init: the network card trifecta
netif_add(&myapp_netif, &ipaddr, &netmask, &gw, NULL, pcapif_init, tcpip_input);
netif_set_default(&myapp_netif);
netif_set_up(&myapp_netif);

The application logic is an independent thread, using the socket API to connect to 1.1.1.1:80 and send an HTTP GET.

lwipcfg.h: Why only these few lines remain. The example_app version has 81 lines (PPP accounts, SLIP parameters, a dozen LWIP_XXX_APP application switches), which my application doesn't need. Which macros does pcapif actually read? I counted them against the source code of pcapif.c: PACKET_LIB_ADAPTER_NR/GUID (select network card), LWIP_MAC_ADDR_BASE (MAC), plus LWIP_PORT_INIT_* used by my own main.c. The rest are dead macros in this project, so I deleted them. (If you want to confirm the answer for your own environment, just grep the contrib/ports/win32/ directory; the source code is the ultimate truth.)

2.3 Two Small Details That Are Easy to Trip Over

Detail 1: Under -Werror, you cannot write return 0; after an infinite loop. The project inherits lwIP's strict warning set (-Werror -Wunreachable-code, etc., from CMakeCommon.cmake). for (;;) is an infinite loop provable by the compiler; any statement following it is unreachable code, directly causing an error. So the main function lets the loop end naturally, without a return (since C99, falling off the end of main is equivalent to return 0).

Detail 2: The assertion hook must follow lwipopts.h. The copied lwipopts.h points LWIP_PLATFORM_ASSERT at the end to an external function lwip_example_app_platform_assert (defined in example_app). My main.c must implement a function with the same name, otherwise a link error occurs. Don't change the name unless you synchronously change lwipopts.h.

Part 3: Pitfall Record (The Debugging Process of the First Build)

Reality check: the two commands in Section 1.4 actually hit three pitfalls in succession on the first execution—one during configuration (Pitfall 1), one while compiling the lwIP library (Pitfall 2), and one while compiling my own main.c (Pitfall 3). The three pitfalls share a commonality: example_app happened to bypass them all, but my minimal project did not—this is exactly the tuition for moving "from running an example to using a library." Let's go through them one by one.

3.1 Pitfall 1: CMake Configuration Errors Out—lwipcontribaddons is a Non-Existent Target

Symptom (the first step cmake -S . -B build -G Ninja blew up immediately):

CMake Error at D:/Projects/lwip/contrib/ports/win32/Filelists.cmake:51 (target_compile_definitions):
  Cannot specify compile definitions for target "lwipcontribaddons" which is
  not built by this project.
Call Stack (most recent call first):
  CMakeLists.txt:38 (include)

Investigation: Looking at the win32 port's Filelists, line 51 is setting compile definitions for a target called lwipcontribaddons. But this target is defined in contrib/Filelists.cmake—and my project intentionally does not include that file (see Section 2.2, this is the implementation of "no examples"). Setting properties on a non-existent target causes CMake to error out immediately. example_app is a full build where that target always exists, so upstream never hits this: this line of code implicitly assumes "you will definitely include contrib's Filelists too," which is an oversight for standalone project scenarios.

The correct fix: Add a "only execute if the target exists" guard to this line. Originally:

target_compile_definitions(lwipcontribaddons PRIVATE ${LWIP_DEFINITIONS} ${LWIP_MBEDTLS_DEFINITIONS})

Changed to:

# The lwipcontribaddons target only exists if contrib/Filelists.cmake is included;
# A standalone project that only includes this port file cannot touch it, so add a "target exists" guard
if(TARGET lwipcontribaddons)
    target_compile_definitions(lwipcontribaddons PRIVATE ${LWIP_DEFINITIONS} ${LWIP_MBEDTLS_DEFINITIONS})
endif()

Zero impact on full builds (behavior is unchanged when the target exists). This is the third local patch for the lwIP repository, following the errno.h and SYSTEM include patches (committed on the fix/win32-filelists-standalone branch)—the git apply command in Section 1.1 applies exactly this.

In a nutshell: When only including partial Filelists, the included script cannot assume the existence of targets defined by other scripts!!

3.2 Pitfall 2: ppp_settings.h—I Have an Ethernet Application, Why Do I Need a PPP Header?

Symptom (configuration passed, but compiling lwipcore caused all those PPP source files to blow up):

D:/Projects/lwip/src/include/netif/ppp/ppp_impl.h:41:10: fatal error: ppp_settings.h: No such file or directory
   41 | #include "ppp_settings.h"
      |          ^~~~~~~~~~~~~~~~
compilation terminated.

Investigation: src/Filelists.cmake puts all PPP source files into the compilation list for lwipcore—note that this is a full set at the file list level. PPP_SUPPORT=0 controls the functional logic, but it cannot prevent the compiler from opening this .c file and executing its #include. And ppp_impl.h unconditionally includes ppp_settings.h: this is a configuration header provided by the application side in lwIP's convention (same category as lwipopts.h). The example_app directory happens to have this file and it's on its include path, so the official build never lacks it.

The correct fix: Copy the ppp_settings.h from example_app to my project's root directory (which is already in LWIP_INCLUDE_DIRS)—the one included in the repository is this copy.

In a nutshell: XXX_SUPPORT=0 disables the functionality, not the compilation list; for the files in the list, not a single required header can be missing!!

3.3 Pitfall 3: 40 errno Macro Redefinitions—Include Order is Also a Bug

Symptom (when compiling my own main.c, about 40 errors in one go, excerpting the first and last):

D:/Projects/lwip/src/include/lwip/errno.h:73:10: error: 'ETXTBSY' redefined [-Werror]
   73 | #define  ETXTBSY         26  /* Text file busy */
C:/msys64/ucrt64/include/errno.h:218:9: note: this is the location of the previous definition
  218 | #define ETXTBSY 139
... (ETXTBSY / EDEADLK / ENOSYS / ECONNRESET / EINPROGRESS etc., about 40 in total, all the same pattern) ...
cc1.exe: all warnings being treated as errors

Does this look very familiar? This is extremely similar to the error we encountered in Part 1. Back then, we used #undef to solve it, which was actually a shortcut—only suppressing the two macros that were triggered. Now the problem has fully erupted, about 40 of them colliding at once, and #undef can't handle it.

The correct fix is to change the order: Place lwIP headers before system headers in the include order. After reversing the order, the redefinition occurs inside UCRT64's system headers. gcc by default does not report warnings in system headers, so the conflict is resolved (this is actually lwIP's convention: lwip/opt.h must be included first). Originally:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "lwip/opt.h"
#include "lwip/init.h"
/* ... other lwIP headers ... */

Changed to:

/* lwIP headers must be placed before system headers: lwip/errno.h (pulled in via lwip/sockets.h) defines
 * its own set of errno values. If the C library's <errno.h> comes in first (on UCRT64, stdlib.h ->
 * malloc.h -> mm_malloc.h will pull it in), lwIP's redefinitions are all errors under -Werror.
 * Conversely, letting lwIP come first means the redefinition happens inside the C library's system headers,
 * which gcc does not report by default */
#include "lwip/opt.h"
#include "lwip/init.h"
/* ... other lwIP headers ... */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

A side effect to mention: This way, the errno macro values seen in main.c will ultimately be the system's (ECONNRESET=108) rather than lwIP's (104), so in application code, do not use errno macros for symbolic comparison, only for numeric printing, and it will be fine.

In a nutshell: When two sets of errno definitions collide, let lwIP's come first—gcc will shut up about redefinitions in system headers!!

Part 4: Verification and Next Steps

4.1 Verification Checklist

The same standard as in the example_app era:

  1. The terminal prints lwIP initialized, local IP is 192.168.0.200, then connected, sending HTTP request, and finally prints the received response bytes.
  2. Another device on the LAN can ping 192.168.0.200 (pinging from the local machine fails due to a fundamental limitation, hairpin filtering, see Part 1 Section 3.5, don't try it from the local machine again).
  3. Optional: Use Wireshark to capture packets and observe the three-way handshake, comparing it with the sequence analyzed in Part 2.

Actual test (2026-08-03, passed in one go after fixing the three pitfalls): Compilation and linking finished cleanly ([92/92] Linking C executable myapp.exe), running ./build/myapp.exe output:

 0: NPF_{9198ECB8-5323-4B4B-8C80-102D46E60528}
     Desc: "WAN Miniport (Network Monitor)"
 ... (the list of local network cards enumerated by pcapif_helper, omitted)...
 4: NPF_{D83D00A3-B69D-4368-ABC0-9B1AAB9F4E61}
     Desc: "Killer E3100G 2.5 Gigabit Ethernet Controller"
 ...
Using adapter_num: 4
Using adapter: "Killer E3100G 2.5 Gigabit Ethernet Controller"
lwIP initialized, local IP is 192.168.0.200
connecting to 1.1.1.1:80 ...
connected, sending HTTP request
received 381 bytes:
HTTP/1.1 301 Moved Permanently
Server: cloudflare
Date: Mon, 03 Aug 2026 08:30:46 GMT
Content-Type: text/html
Content-Length: 167
Connection: close
Location: https://1.1.1.1/
CF-RAY: a253f6553d64da3a-LAX

<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>cloudflare</center>
</body>
</html>

myapp client finished.

The list of network cards at the beginning is enumerated by pcapif_helper.c for you to match against the GUID—the "copy it from here" mentioned in Section 1.3 refers to this. The GUID filled in lwipcfg.h matched index 4, the Killer E3100G, so the card selection was correct. The following lines are the full link verification: protocol stack initialization obtains an IP → TCP three-way handshake connects to 1.1.1.1:80 → sends HTTP GET → receives a 301 response.

4.2 After Getting It Running: The Tailoring Roadmap

After obtaining the baseline, the tailoring of lwipopts.h truly begins. The suggested order is from large to small: LWIP_IPV6 0PPP_SUPPORT 0 → unused statistics/debug options. Recompile after each change—if it compiles, the tailoring is self-consistent; run the client once to ensure behavior isn't broken. However, on Windows, this step is purely the icing on the cake; after all, if it runs, it's fine. The real pressure for tailoring only comes on an embedded target.

Summary and Preview

At this point, lwIP has transformed from "the example I ran" to "a library linked into my project." Reviewing the two most valuable insights:

The next part will flip lwIP around to act as a server, implementing a simple wooden fish tapping app.

Acknowledgements

Thanks to the lwIP author's BUILDING file, from which all inspiration for this article originates.


Environment: Windows 11 / MSYS2 UCRT64 gcc 16.1.0 / lwIP 2.2.2 / Npcap SDK 1.16

Comments

Top 3 of 4 from juejin.cn, machine-translated. The original thread is authoritative.

疏赎蜀黍 1 likes

[Heart][Heart][Heart]

NiCo

[Heart][Heart]

浪花一朵朵2026

Can the entire source code be downloaded from the repository?

NiCo

Found that my own repository link can't display properly [crying][crying][crying], can only fix it tomorrow [dizzy][dizzy]