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:
- Dynamic Loading: Manually load at runtime using the
NativeLibraryAPI - Static Loading: Declare using the
DllImportattribute (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 pathNativeLibrary.GetExport- Gets the pointer of the exported function from the loaded libraryMarshal.GetDelegateForFunctionPointer- Converts an unmanaged function pointer to a .NET delegate, enabling calling native functions like normal methodsMarshal.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 StudioCondition="'$(RuntimeIdentifier)' == 'linux-x64'"- When publishing with-r linux-x64, copies the Linux library- The
Linkattribute 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.
Scenario 2: Multi-Project + Conditional Compilation (✅ Success, Recommended)
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:
- Directory.Build.props is not packaged: The NuGet package will not include the
Directory.Build.propsfrom the repository root, so upstream users cannot inherit the macro definitions - 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
- Upstream cannot change behavior: Projects that install the NuGet package, even if they define their own
PLATFORM_XXXmacros, 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).
Scenario 3: Multi-Project + Only Library Name (✅ Recommended)
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 forLib/TimeMeaning.dll - Linux:
DllImport("Lib/TimeMeaning")looks forLib/TimeMeaningorLib/TimeMeaning.so, notLib/libTimeMeaning.so - So on Linux, use
<Link>Lib\TimeMeaning.so</Link>to copylibTimeMeaning.soasTimeMeaning.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
- 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
- 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)
- In multi-project scenarios, the macro inheritance issue can be resolved with a publishing script: Use
publish.bat+SetPlatformMacro.ps1to modify global macros before publishing - 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
- On Linux, remember to remove the
libprefix by renaming via the<Link>mechanism in csproj - To support Windows 7, install the VC-LTL and YY-Thunks NuGet packages
- Library files can be placed in a Lib subdirectory, not necessarily in the root directory
- ⚠️ 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
- NuGet is essentially a compile output: It contains dll + metadata, not build configuration
- Macros are compile-time concepts:
#ifcompletes branch pruning at compile time, no longer exists at runtime - 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).
Approach B: Do not use conditional compilation macros inside the library (Recommended)
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-x64YourLibrary.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 forTimeMeaning,TimeMeaning.so,libTimeMeaning.so- no need to remove prefix - Library in subdirectory:
DllImport("Lib/TimeMeaning")on Linux only looks forLib/TimeMeaning,Lib/TimeMeaning.so, notLib/libTimeMeaning.so. Therefore, you need to use the<Link>mechanism in csproj to copylibTimeMeaning.soasTimeMeaning.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 stackWinapi: 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:
- Check if the correct library file exists in the
Lib/subdirectory of the output directory - Use Process Monitor (Windows) or lsof (Linux) to monitor library file loading
- 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