.NET Cross-Platform Native Library Integration in Practice

.NET Cross-Platform Native Library Integration in Practice

In-depth analysis of how to elegantly introduce third-party native libraries in .NET projects, supporting Windows, Linux multi-platform, with a pitfall avoidance guide

Last updated 4/20/2026 11:18 PM
沙漠尽头的狼
22 min read
Category
.NET
Tags
.NET C# Cross-Platform Native Library

When developing with .NET, there are occasional needs to call native libraries (Native Library) provided by third parties, such as hardware SDKs, encryption libraries, or low-level communication components. Through an actual Demo project, this article shares two major approaches and experience in introducing cross-platform native libraries.

1. Project Preparation

The Demo project uses a simple C++ dynamic library called TimeMeaning, which provides one API:

// Input a timestamp in seconds, returns a human/ time-implication text
const char* GetTimeMeaning(int timestampSecond);

The timestamp is modulo 10 and returns the corresponding text, implying that every moment has a different life insight:

static const char* TIME_MEANINGS[] = {
    "黎明破晓,万物苏醒,新的一天带来新的希望",
    "晨光熹微,思绪清晰,适合规划一天的行程",
    "日出东方,阳光灿烂,充满活力与朝气",
    "上午时光,精力充沛,专注做事效率高",
    "正午时分,阳光明媚,适合休息片刻",
    "午后暖阳,慵懒惬意,时光静静流淌",
    "夕阳西下,余晖满天,美好的黄昏时分",
    "夜幕降临,星光点点,思绪开始沉淀",
    "夜深人静,皓月当空,适合反思与冥想",
    "午夜时分,万籁俱寂,梦想在黑暗中萌芽"
};

Third-party libraries usually provide versions for different platforms, with a directory structure as follows:

Lib/
├── x64/
│   ├── TimeMeaning.dll        # 64-bit Windows
│   └── (lib)TimeMeaning.so    # 64-bit Linux
├── x86/
│   └── TimeMeaning.dll        # 32-bit Windows
└── arm64/
    └── (lib)TimeMeaning.so    # ARM64 Linux

We want:

  • Code to remain uniform: No need to write different calling code for each platform
  • Automatic adaptation: Automatically select the corresponding platform library file during compilation or publishing

Directory.Build.props Global Macro Definition

First, create Directory.Build.props at the solution root, default defining the PLATFORM_WIN_X86 conditional compilation macro:

<Project>
	<PropertyGroup>
		<DefineConstants>$(DefineConstants);PLATFORM_WIN_X86</DefineConstants>
	</PropertyGroup>
</Project>

Then use the publish.bat publishing script to call SetPlatformMacro.ps1, which modifies the global macro definition based on the target platform before publishing:

# SetPlatformMacro.ps1
$macro = ""
switch ($Platform) {
    "linux-x64" { $macro = "PLATFORM_LINUX_X64" }
    "linux-arm64" { $macro = "PLATFORM_LINUX_ARM64" }
    "win-x64" { $macro = "PLATFORM_WIN_X64" }
    "win-x86" { $macro = "PLATFORM_WIN_X86" }
}
$content = $content -replace '<DefineConstants>.*?</DefineConstants>', "<DefineConstants>`$(DefineConstants);$macro</DefineConstants>"

This allows easy distinction of the currently compiled platform version in code:

#if PLATFORM_WIN_X64
var platform = "Windows X64";
#elif PLATFORM_WIN_X86
var platform = "Windows X86";
#elif PLATFORM_LINUX_X64
var platform = "Linux X64";
#elif PLATFORM_LINUX_ARM64
var platform = "Linux ARM64";
#else
var platform = "Unknown";
#endif

2. Two Major Approaches Overview

Introducing native libraries mainly involves two major approaches:

  1. Dynamic Loading: Manually load at runtime using the NativeLibrary API
  2. Static Loading: Declare using the DllImport attribute (tested in 3 scenarios)

VC-LTL and YY-Thunks (Common to All Examples)

All 4 sample programs include the following two NuGet packages. The tested examples support Windows 7+ and Linux platforms:

Windows 7 Operating Principle: Although Microsoft officially no longer supports Windows 7 with .NET 10, by using the net10.0-windows target framework and AOT publishing, the program can still run on Windows 7. AOT compiles .NET code statically into a native executable, completely removing the dependency on the .NET runtime; combined with VC-LTL and YY-Thunks, which provide lightweight C runtime support and a compatibility layer for old Windows APIs, cross-version compatibility is achieved.

<PackageReference Include="VC-LTL" Version="5.3.1" />
<PackageReference Include="YY-Thunks" Version="1.2.1-Beta.4" />
  • VC-LTL: Uses an open-source VC runtime library, eliminating the need for system patches, significantly reducing program size, and compatible with older systems. When combined with AOT (PublishAot=true) publishing, it eliminates .NET runtime dependencies, directly generating a native executable.
  • YY-Thunks: Provides a compatibility layer for new APIs on older Windows, allowing modern code to run smoothly on Win7/XP (XP not tested in the article's examples).

3. Approach 1: Dynamic Loading (✅ Success)

Dynamic loading is the most flexible approach, using the NativeLibrary API to manually load the native library at runtime. This approach allows full customization of the library loading logic, handling complex scenarios where the same library is stored in different directories or uses different file names. For certain special requirements, such as needing to load different versions dynamically at runtime or where the library path needs to be calculated dynamically, dynamic loading is the best choice.

Code Implementation

using System.Runtime.InteropServices;

namespace csharp.test.dynamic;

internal static class TimeMeaningNative
{
    // Determine the library file name based on the current operating system: dll for Windows, so for Linux
    private static readonly string DllName = OperatingSystem.IsWindows()
        ? "TimeMeaning.dll"
        : "libTimeMeaning.so";

    // Define a delegate with the same calling convention as the C++ function, for later conversion
    [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
    private delegate IntPtr GetTimeMeaningDelegate(int timestampSecond);

    // Used to store the converted delegate (used when calling)
    private static GetTimeMeaningDelegate? _getTimeMeaning;
    // Used to store the library handle (used for later freeing)
    private static IntPtr _handle;

    // Static constructor, automatically executed when the class is first used
    static TimeMeaningNative()
    {
        // Concatenate the full path of the library: application root directory + Lib subdirectory + file name
        var dllPath = Path.Combine(AppContext.BaseDirectory, "Lib", DllName);

        // NativeLibrary.Load: loads the native library from the given path, returns a library handle
        _handle = NativeLibrary.Load(dllPath);

        // NativeLibrary.GetExport: gets the address of the exported function with the given name from the loaded library
        var funcPtr = NativeLibrary.GetExport(_handle, "GetTimeMeaning");

        // Marshal.GetDelegateForFunctionPointer: converts the function pointer to a callable delegate
        _getTimeMeaning = Marshal.GetDelegateForFunctionPointer<GetTimeMeaningDelegate>(funcPtr);
    }

    // Provides a method to manually free the library to avoid memory leaks
    public static void Free()
    {
        if (_handle == IntPtr.Zero) return;
        NativeLibrary.Free(_handle);
        _handle = IntPtr.Zero;
    }

    // Wraps the external call interface, returning a string result
    public static string GetTimeMeaningString(int timestampSecond)
    {
        if (_getTimeMeaning == null)
        {
            throw new InvalidOperationException("Dynamic library not loaded correctly");
        }

        // Call the delegate, get the pointer returned by the C++ function
        var ptr = _getTimeMeaning(timestampSecond);
        // Convert the UTF8 encoded character pointer to a C# string
        return Marshal.PtrToStringUTF8(ptr) ?? string.Empty;
    }
}

Code Explanation:

  • NativeLibrary.Load - Core API, loads the native library from the full path
  • NativeLibrary.GetExport - Gets the pointer of the exported function from the loaded library
  • Marshal.GetDelegateForFunctionPointer - Converts an unmanaged function pointer to a .NET delegate, enabling calling native functions like normal methods
  • Marshal.PtrToStringUTF8 - Converts the UTF8 string pointer returned by C++ to a C# string

Project Configuration (csproj)

<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<OutputType>Exe</OutputType>
		<TargetFrameworks>net10.0;net10.0-windows</TargetFrameworks>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>

		<WindowsSupportedOSPlatformVersion>6.1</WindowsSupportedOSPlatformVersion>
		<TargetPlatformMinVersion>6.1</TargetPlatformMinVersion>
	</PropertyGroup>

	<ItemGroup>
		<PackageReference Include="CodeWF.Log.Core" Version="11.3.14" />
		<PackageReference Include="VC-LTL" Version="5.3.1" />
		<PackageReference Include="YY-Thunks" Version="1.2.1-Beta.4" />
	</ItemGroup>

	<ItemGroup>
		<!-- Debug mode defaults to copying Win x64 library, convenient for local debugging -->
		<None Update="Lib\x64\TimeMeaning.dll" Condition="'$(Configuration)' == 'Debug'">
			<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
			<Link>Lib\TimeMeaning.dll</Link>
		</None>
	</ItemGroup>
	<ItemGroup>
		<None Update="Lib\x64\libTimeMeaning.so" Condition="'$(RuntimeIdentifier)' == 'linux-x64'">
			<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
			<Link>Lib\libTimeMeaning.so</Link>
		</None>
		<None Update="Lib\x64\TimeMeaning.dll" Condition="'$(RuntimeIdentifier)' == 'win-x64'">
			<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
			<Link>Lib\TimeMeaning.dll</Link>
		</None>
		<None Update="Lib\x86\TimeMeaning.dll" Condition="'$(RuntimeIdentifier)' == 'win-x86'">
			<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
			<Link>Lib\TimeMeaning.dll</Link>
		</None>
	</ItemGroup>

</Project>

csproj Configuration Explanation:

  • Condition="'$(Configuration)' == 'Debug'" - In Debug mode, default copies the Windows x64 library, convenient for debugging in Visual Studio
  • Condition="'$(RuntimeIdentifier)' == 'linux-x64'" - When publishing with -r linux-x64, copies the Linux library
  • The Link attribute specifies the path in the output directory, ensuring consistency with the path loaded in code

Key Points

Dynamic Loading Process:

  • Determine library file name based on operating system at runtime
  • Concatenate full path and load library
  • Get exported function address
  • Convert to delegate and call

4. Approach 2: Static Loading

Static loading uses the DllImport attribute declaration, which is the standard way to call native libraries in .NET. We tested 3 scenarios, mainly to verify whether using conditional compilation macros is feasible and how flexible the path is when the third-party library wrapper code is placed directly in the main project or extracted and distributed via NuGet.

Two Ways to Define Conditional Compilation Macros

Before using conditional compilation macros, the PLATFORM_XXX macro needs to be defined. There are two ways:

Method 1: Define in Publish Profile (pubxml)

Add to <PropertyGroup> in Properties/PublishProfiles/*.pubxml:

<PropertyGroup>
    <DefineConstants>$(DefineConstants);PLATFORM_WIN_X64</DefineConstants>
</PropertyGroup>

Method 2: Define in Project File (csproj)

Add to <PropertyGroup> in the project file:

<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64'">
    <DefineConstants>$(DefineConstants);PLATFORM_LINUX_X64</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'linux-arm64'">
    <DefineConstants>$(DefineConstants);PLATFORM_LINUX_ARM64</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64'">
    <DefineConstants>$(DefineConstants);PLATFORM_WIN_X64</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x86'">
    <DefineConstants>$(DefineConstants);PLATFORM_WIN_X86</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition="'$(RuntimeIdentifier)' == ''">
    <DefineConstants>$(DefineConstants);PLATFORM_EMPTY</DefineConstants>
</PropertyGroup>

Important Note: The conditional compilation macro definitions added in Method 2 only take effect in the main project. Because when a sub-project is compiled, $(RuntimeIdentifier) is empty (it does not inherit the main project's RID), the sub-project will match the $(RuntimeIdentifier) == '' condition and define PLATFORM_EMPTY. This means you cannot correctly distinguish platforms in sub-projects using this approach. If you need to use platform macros in sub-projects, you need to combine with a publishing script to dynamically modify the global macro definition in Directory.Build.props (see Scenario 2).

Scenario 1: Single Project + Conditional Compilation (✅ Success)

Directly use DllImport in the main project, set the library path via conditional compilation macros. Suitable for scenarios where you don't want to wrap as a class library, such as small tools or projects with low reusability.

Advantage of static loading with conditional compilation macros: Flexible handling of completely different library names on different platforms, including different directories (possible in real scenarios). For example, Windows uses Lib/Windows x64/TimeMeaning.dll, Linux uses Lib/Linux x64/libTimeMeaning.so. This is somewhat similar to Approach 1's dynamic loading, both can handle complex path differences.

Code Implementation

using System.Runtime.InteropServices;

namespace csharp.test.static_;

internal static class TimeMeaningNative
{
#if PLATFORM_WIN_X64 || PLATFORM_WIN_X86
    const string DLL = "Lib/TimeMeaning.dll";
#elif PLATFORM_LINUX_X64 || PLATFORM_LINUX_ARM64
    const string DLL = "Lib/libTimeMeaning.so";
#else
    const string DLL = "Lib/TimeMeaning.dll";
#endif

    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
    private static extern IntPtr GetTimeMeaning(int timestampSecond);

    public static string GetTimeMeaningString(int timestampSecond)
    {
        var ptr = GetTimeMeaning(timestampSecond);
        return Marshal.PtrToStringUTF8(ptr) ?? string.Empty;
    }
}

Result

Success: In a single-project scenario, conditional compilation macros work correctly. Both Windows and Linux load the corresponding library files properly.


Encapsulate the library calling into a separate class library project, then reference it from the main project. Use the publish.bat publishing script to call SetPlatformMacro.ps1 before publishing to modify the global macro definition, solving the issue where class libraries do not inherit conditional compilation macros.

Class Library Code (TimeMeaningNative.csproj)

using System.Runtime.InteropServices;

namespace TimeMeaningNative;

public static class TimeMeaningApi
{
#if PLATFORM_WIN_X64 || PLATFORM_WIN_X86
    const string DLL = "Lib/TimeMeaning.dll";
#elif PLATFORM_LINUX_X64 || PLATFORM_LINUX_ARM64
    const string DLL = "Lib/libTimeMeaning.so";
#else
    const string DLL = "Lib/TimeMeaning.dll";
#endif

    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
    private static extern IntPtr GetTimeMeaning(int timestampSecond);

    public static string GetTimeMeaningString(int timestampSecond)
    {
        var ptr = GetTimeMeaning(timestampSecond);
        return Marshal.PtrToStringUTF8(ptr) ?? string.Empty;
    }
}

Publishing Script (publish.bat)

for %%p in (%platforms%) do (
    set "tfm="
    set "pubxml="

    if "%%p"=="linux-x64" set "tfm=net10.0" & set "pubxml=FolderProfile_linux-x64.pubxml"
    ...

    powershell -ExecutionPolicy Bypass -File "SetPlatformMacro.ps1" -Platform "%%p"

    for %%d in (%project_paths%) do (
        dotnet publish "%%d" -f !tfm! /p:PublishProfile="%%d\Properties\PublishProfiles\!pubxml!"
    )
)

Reason for Success

Success: The publish.bat script invokes SetPlatformMacro.ps1 before publishing to modify the global macro definition in Directory.Build.props, so sub-projects also correctly receive the platform macro (e.g., PLATFORM_LINUX_X64), rather than relying on RuntimeIdentifier at compile time.

Note: The specific scripts can be found in the test repository; the repository address is at the end of the article.

⚠️ Important: NuGet Packaging Limitations

Note: This approach is only suitable for referencing multiple projects within the same source repository. If you package this class library as a NuGet package and distribute it to other projects, the following issues arise:

  1. Directory.Build.props is not packaged: The NuGet package will not include the Directory.Build.props from the repository root, so upstream users cannot inherit the macro definitions
  2. Macros are already fixed at compile time: When packaging, the code has already completed branch pruning based on the macros at that time; the generated dll is no longer affected by upstream macros
  3. Upstream cannot change behavior: Projects that install the NuGet package, even if they define their own PLATFORM_XXX macros, cannot change the code branches of the already packaged library

Solution: If you need to distribute cross-platform libraries via NuGet, it is recommended to use Approach 4 (Multi-project + Only Library Name), or use the NuGet build props mechanism (see supplementary explanation below).


This is the most recommended approach, reducing complexity compared to dynamic loading. The class library does not use conditional compilation macros, only specifies the library name (without extension), solving cross-platform library reference issues.

Class Library Code (TimeMeaningNative.csproj)

using System.Runtime.InteropServices;

namespace TimeMeaningNative;

public static class TimeMeaningApi
{
    const string DLL = "Lib/TimeMeaning";
    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
    private static extern IntPtr GetTimeMeaning(int timestampSecond);

    public static string GetTimeMeaningString(int timestampSecond)
    {
        var ptr = GetTimeMeaning(timestampSecond);
        return Marshal.PtrToStringUTF8(ptr) ?? string.Empty;
    }
}

Main Project Configuration (csproj)

Here's a key trick: If the Linux library has a lib prefix, remove it, and change the Windows dll to the same file name. When copying under Linux, remove the lib prefix!

<ItemGroup>
    <!-- Linux key: Remove lib prefix when copying! -->
    <None Update="Lib\x64\libTimeMeaning.so" Condition="'$(RuntimeIdentifier)' == 'linux-x64'">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        <Link>Lib\TimeMeaning.so</Link>
    </None>
    ...
</ItemGroup>

How It Works

  • Windows: DllImport("Lib/TimeMeaning") automatically looks for Lib/TimeMeaning.dll
  • Linux: DllImport("Lib/TimeMeaning") looks for Lib/TimeMeaning or Lib/TimeMeaning.so, not Lib/libTimeMeaning.so
  • So on Linux, use <Link>Lib\TimeMeaning.so</Link> to copy libTimeMeaning.so as TimeMeaning.so

Result

Success: This is the most recommended approach, simple and reliable. No need to define conditional compilation macros; just ensure the library file names are unified across platforms (remove the lib prefix on Linux).


Approach Comparison Summary

Category Approach Method Result Applicable Scenario
Dynamic Loading NativeLibrary dynamic loading Manually determine OS and load library in code ✅ All platforms available Need flexible control over loading path
Static Loading Single project + conditional compilation #if PLATFORM_WIN_X64 conditional compilation ✅ Success Main project only, no class library, supports different paths
Static Loading Multi-project + conditional compilation Global macro set via script before publishing Success (with publish script) Recommended, different library paths needed
Static Loading Multi-project + only library name (no extension) Class library writes only name + csproj conditional copy (Linux removes lib prefix) Cross-platform perfect success Most recommended, simple and reliable, desired unified file names

6. Core Experience

  1. Use DllImport with a constant library name (without extension) - this is the most stable and reliable approach, simple and easy to understand. Dynamic loading (Approach 1) also works but is slightly more complex to use
  2. Static loading with conditional compilation macros can handle different library names, suitable for both single and multi-project scenarios (multi-project requires a publishing script to set macros globally)
  3. In multi-project scenarios, the macro inheritance issue can be resolved with a publishing script: Use publish.bat + SetPlatformMacro.ps1 to modify global macros before publishing
  4. Do not rely on the class library's compile-time RuntimeIdentifier, because the class library may not have a RuntimeIdentifier context during compilation, causing conditional compilation macros to not take effect
  5. On Linux, remember to remove the lib prefix by renaming via the <Link> mechanism in csproj
  6. To support Windows 7, install the VC-LTL and YY-Thunks NuGet packages
  7. Library files can be placed in a Lib subdirectory, not necessarily in the root directory
  8. ⚠️ Important: Directory.Build.props global macros do not support NuGet distribution: If you package a class library that uses conditional compilation macros as a NuGet, upstream projects cannot inherit the macros, and the code inside the NuGet package will have its branches fixed at packaging time. For NuGet distribution, it is recommended to use Approach 4 (only library name, no conditional compilation macros)

Supplementary Note: For beginners, mastering Scenario 3 (Multi-project + Only Library Name) is the best choice—it's the most straightforward and easy to understand. If you really need to handle path differences flexibly, then consider dynamic loading or Scenario 2.

7. Supplementary: NuGet Packaging and Conditional Compilation Macros

7.1 Directory.Build.props and NuGet Limitations

Conclusion: Directory.Build.props is a build configuration at the source repository level, not part of the project itself, and certainly not part of the NuGet package.

Scope of Effect

Scenario Effective? Description
Local multi-project development ✅ Yes MSBuild automatically traverses upward from project dir
Compilation during packaging ✅ Yes During packaging compilation, macro is applied to prune branches
Inside NuGet package (runtime) ❌ No Macro has disappeared after compilation, dll branches are fixed
After upstream project installs ❌ No NuGet does not contain Directory.Build.props, cannot inherit

Core Reason

  1. NuGet is essentially a compile output: It contains dll + metadata, not build configuration
  2. Macros are compile-time concepts: #if completes branch pruning at compile time, no longer exists at runtime
  3. Directory.Build.props is not packaged: It only takes effect in the local repository structure

7.2 Alternative Approaches for NuGet Distribution

Approach A: Use NuGet build props (pass macros downstream)

If you really need to pass macro definitions downstream via NuGet, add a build/ folder in the library project:

<!-- build/YourPackageName.props -->
<Project>
  <PropertyGroup>
    <!-- Automatically define macros based on RuntimeIdentifier -->
    <DefineConstants Condition="'$(RuntimeIdentifier)'=='win-x86'">
      $(DefineConstants);PLATFORM_WIN_X86
    </DefineConstants>
    <DefineConstants Condition="'$(RuntimeIdentifier)'=='win-x64'">
      $(DefineConstants);PLATFORM_WIN_X64
    </DefineConstants>
    <DefineConstants Condition="'$(RuntimeIdentifier)'=='linux-x64'">
      $(DefineConstants);PLATFORM_LINUX_X64
    </DefineConstants>
    <DefineConstants Condition="'$(RuntimeIdentifier)'=='linux-arm64'">
      $(DefineConstants);PLATFORM_LINUX_ARM64
    </DefineConstants>
  </PropertyGroup>
</Project>

Then configure packaging in the library project's csproj:

<ItemGroup>
  <None Include="build\**" Pack="true" PackagePath="build\" />
</ItemGroup>

Note: This only allows downstream projects to obtain the macro definitions, but it cannot change the code branches inside the already packaged library (because the library dll is already fixed at compile time).

The safest NuGet distribution approach is to avoid using conditional compilation macros in the library code, and instead use Approach 4 (only library name) or runtime determination:

// Recommended: use runtime determination or only library name inside the library
public static class TimeMeaningApi
{
    const string DLL = "Lib/TimeMeaning"; // without extension

    [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
    private static extern IntPtr GetTimeMeaning(int timestampSecond);

    // ...
}

Approach C: Package NuGet separately by RID

Package NuGet separately for each platform, using different package IDs or versions:

  • YourLibrary.win-x64
  • YourLibrary.linux-x64
  • etc.

However, this increases maintenance costs and is generally not recommended.

7.3 Final Suggestions

  • Multiple projects within the same repository: Can use Directory.Build.props + publishing script (Scenario 2)
  • Need NuGet distribution: Strongly recommend Approach 4 (only library name, no dependency on conditional compilation macros)
  • Must use conditional compilation + NuGet: Consider combining NuGet build props with runtime determination

8. Common Questions Q&A

Q1: Why remove the lib prefix on Linux?

A: Two cases:

  • Library in root directory: DllImport("TimeMeaning") on Linux will look for TimeMeaning, TimeMeaning.so, libTimeMeaning.so - no need to remove prefix
  • Library in subdirectory: DllImport("Lib/TimeMeaning") on Linux only looks for Lib/TimeMeaning, Lib/TimeMeaning.so, not Lib/libTimeMeaning.so. Therefore, you need to use the <Link> mechanism in csproj to copy libTimeMeaning.so as TimeMeaning.so

Q2: Must the library file be in the same directory as the executable?

A: No! It can be in a subdirectory (e.g., Lib/). Simply specify the subdirectory path in DllImport, such as DllImport("Lib/TimeMeaning"). Note that when using a subdirectory, Linux will not look for libraries with the lib prefix.

Q3: What does CallingConvention mean?

A: It defines how parameters are passed and the stack is cleaned up when calling a function. Common ones include:

  • Cdecl: Default C convention, caller cleans the stack (common on Linux)
  • StdCall: Common in Windows API, callee cleans the stack
  • Winapi: Platform default (StdCall on Windows, Cdecl on Linux)

Q4: Is macOS supported?

A: Yes! macOS uses the .dylib suffix. You can also use DllImport("Lib/TimeMeaning"); the system will automatically look for Lib/TimeMeaning.dylib. Simply add configurations for osx-x64 and osx-arm64 in csproj.

Q5: How can I verify during debugging that the library file is correctly loaded?

A: You can use the following methods:

  1. Check if the correct library file exists in the Lib/ subdirectory of the output directory
  2. Use Process Monitor (Windows) or lsof (Linux) to monitor library file loading
  3. Test loading success in code by calling NativeLibrary.TryLoad

The above content is organized based on an actual Demo project, including C++ library code and complete code examples for the four major approaches. If there are any errors or better approaches, feel free to leave a comment to point them out!

Open source project address: https://github.com/dotnet9/DotnetCrossPlatformNativeLibrary

Keep Exploring

Related Reading

More Articles