├── .github └── workflows │ ├── greetings.yml │ └── stale.yml ├── .gitignore ├── LICENSE.md ├── LibLoader.sln ├── README.md ├── samples └── LibLoader.Samples.Basic │ ├── LibLoader.Samples.Basic.csproj │ ├── LocalFileServer.cs │ ├── Program.cs │ └── libMyNativeLib.dll └── src ├── Builders ├── DependencyDefinitionBuilder.cs ├── LibraryDefinitionBuilder.cs ├── NativeFunctionDefinitionBuilder.cs └── PlatformBuilder.cs ├── Exceptions ├── LibraryLoadException.cs ├── MissingFunctionException.cs ├── MissingLibraryException.cs ├── NativeFunctionCallException.cs ├── UnsupportedArchitectureException.cs ├── UnsupportedParameterTypeException.cs └── UnsupportedPlatformException.cs ├── Interfaces ├── ILibraryDefinitionBuilder.cs ├── INativeFunctionBuilder.cs └── IPlatformBuilder.cs ├── LibLoader.cs ├── LibLoader.csproj ├── Models ├── LibraryDefinition.cs └── NativeFunctionDefinition.cs ├── Native ├── DelegateFactory.cs └── ParameterMarshaler.cs └── libloader-icon.png /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: pull_request_target 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | pull-requests: write 10 | steps: 11 | - uses: actions/first-interaction@v1 12 | with: 13 | repo-token: ${{ secrets.GITHUB_TOKEN }} 14 | pr-message: | 15 | Welcome to LibLoader repository! We appreciate you taking the time to contribute. 16 | 17 | We're excited to review your pull request and look forward to collaborating with you. Please let us know if you have any questions or need any assistance. 18 | 19 | Thank you for your contribution! -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '44 8 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v5 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | stale-issue-message: | 25 | This issue has been automatically marked as stale because it has not had recent activity. 26 | It will be closed if no further activity occurs. 27 | 28 | If this issue is still relevant, please leave a comment indicating that you would like it to remain open. 29 | Thank you for your contributions. 30 | stale-pr-message: | 31 | This pull request has been automatically marked as stale because it has not had recent activity. 32 | It will be closed if no further activity occurs. 33 | 34 | If you are still working on this pull request, please leave a comment indicating that you would like it to remain open. 35 | Thank you for your contributions. 36 | stale-issue-label: 'no-issue-activity' 37 | stale-pr-label: 'no-pr-activity' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | .DS_Store 13 | [Ea]xtras/ 14 | EGOIST.Deparcated/ 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Bb]uild/ 24 | [Dd]ebug/ 25 | [Dd]ebugPublic/ 26 | [Rr]elease/ 27 | [Rr]eleases/ 28 | x64/ 29 | x86/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | 39 | # Visual Studio 2015/2017 cache/options directory 40 | .vs/ 41 | .idea/ 42 | # Uncomment if you have tasks that create the project's static files in wwwroot 43 | #wwwroot/ 44 | 45 | # Visual Studio 2017 auto generated files 46 | Generated\ Files/ 47 | 48 | # MSTest test Results 49 | [Tt]est[Rr]esult*/ 50 | [Bb]uild[Ll]og.* 51 | 52 | # NUnit 53 | *.VisualState.xml 54 | TestResult.xml 55 | nunit-*.xml 56 | 57 | # Build Results of an ATL Project 58 | [Dd]ebugPS/ 59 | [Rr]eleasePS/ 60 | dlldata.c 61 | 62 | # Benchmark Results 63 | BenchmarkDotNet.Artifacts/ 64 | 65 | # .NET Core 66 | project.lock.json 67 | project.fragment.lock.json 68 | artifacts/ 69 | 70 | # StyleCop 71 | StyleCopReport.xml 72 | 73 | # Files built by Visual Studio 74 | *_i.c 75 | *_p.c 76 | *_h.h 77 | *.ilk 78 | *.meta 79 | *.obj 80 | *.iobj 81 | *.pch 82 | *.pdb 83 | *.ipdb 84 | *.pgc 85 | *.pgd 86 | *.rsp 87 | *.sbr 88 | *.tlb 89 | *.tli 90 | *.tlh 91 | *.tmp 92 | *.tmp_proj 93 | *_wpftmp.csproj 94 | *.log 95 | *.vspscc 96 | *.vssscc 97 | .builds 98 | *.pidb 99 | *.svclog 100 | *.scc 101 | 102 | # Chutzpah Test files 103 | _Chutzpah* 104 | 105 | # Visual C++ cache files 106 | ipch/ 107 | *.aps 108 | *.ncb 109 | *.opendb 110 | *.opensdf 111 | *.sdf 112 | *.cachefile 113 | *.VC.db 114 | *.VC.VC.opendb 115 | 116 | # Visual Studio profiler 117 | *.psess 118 | *.vsp 119 | *.vspx 120 | *.sap 121 | 122 | # Visual Studio Trace Files 123 | *.e2e 124 | 125 | # TFS 2012 Local Workspace 126 | $tf/ 127 | 128 | # Guidance Automation Toolkit 129 | *.gpState 130 | 131 | # ReSharper is a .NET coding add-in 132 | _ReSharper*/ 133 | *.[Rr]e[Ss]harper 134 | *.DotSettings.user 135 | 136 | # TeamCity is a build add-in 137 | _TeamCity* 138 | 139 | # DotCover is a Code Coverage Tool 140 | *.dotCover 141 | 142 | # AxoCover is a Code Coverage Tool 143 | .axoCover/* 144 | !.axoCover/settings.json 145 | 146 | # Visual Studio code coverage results 147 | *.coverage 148 | *.coveragexml 149 | 150 | # NCrunch 151 | _NCrunch_* 152 | .*crunch*.local.xml 153 | nCrunchTemp_* 154 | 155 | # MightyMoose 156 | *.mm.* 157 | AutoTest.Net/ 158 | 159 | # Web workbench (sass) 160 | .sass-cache/ 161 | 162 | # Installshield output folder 163 | [Ee]xpress/ 164 | 165 | # DocProject is a documentation generator add-in 166 | DocProject/buildhelp/ 167 | DocProject/Help/*.HxT 168 | DocProject/Help/*.HxC 169 | DocProject/Help/*.hhc 170 | DocProject/Help/*.hhk 171 | DocProject/Help/*.hhp 172 | DocProject/Help/Html2 173 | DocProject/Help/html 174 | 175 | # Click-Once directory 176 | publish/ 177 | 178 | # Publish Web Output 179 | *.[Pp]ublish.xml 180 | *.azurePubxml 181 | # Note: Comment the next line if you want to checkin your web deploy settings, 182 | # but database connection strings (with potential passwords) will be unencrypted 183 | *.pubxml 184 | *.publishproj 185 | 186 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 187 | # checkin your Azure Web App publish settings, but sensitive information contained 188 | # in these scripts will be unencrypted 189 | PublishScripts/ 190 | 191 | # NuGet Packages 192 | *.nupkg 193 | # NuGet Symbol Packages 194 | *.snupkg 195 | # The packages folder can be ignored because of Package Restore 196 | **/[Pp]ackages/* 197 | # except build/, which is used as an MSBuild target. 198 | !**/[Pp]ackages/build/ 199 | # Uncomment if necessary however generally it will be regenerated when needed 200 | #!**/[Pp]ackages/repositories.config 201 | # NuGet v3's project.json files produces more ignorable files 202 | *.nuget.props 203 | *.nuget.targets 204 | 205 | # Microsoft Azure Build Output 206 | csx/ 207 | *.build.csdef 208 | 209 | # Microsoft Azure Emulator 210 | ecf/ 211 | rcf/ 212 | 213 | # Windows Store app package directories and files 214 | AppPackages/ 215 | BundleArtifacts/ 216 | Package.StoreAssociation.xml 217 | _pkginfo.txt 218 | *.appx 219 | *.appxbundle 220 | *.appxupload 221 | 222 | # Visual Studio cache files 223 | # files ending in .cache can be ignored 224 | *.[Cc]ache 225 | # but keep track of directories ending in .cache 226 | !?*.[Cc]ache/ 227 | 228 | # Others 229 | ClientBin/ 230 | ~$* 231 | *~ 232 | *.dbmdl 233 | *.dbproj.schemaview 234 | *.jfm 235 | *.pfx 236 | *.publishsettings 237 | orleans.codegen.cs 238 | 239 | # Including strong name files can present a security risk 240 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 241 | #*.snk 242 | 243 | # Since there are multiple workflows, uncomment next line to ignore bower_components 244 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 245 | #bower_components/ 246 | 247 | # RIA/Silverlight projects 248 | Generated_Code/ 249 | 250 | # Backup & report files from converting an old project file 251 | # to a newer Visual Studio version. Backup files are not needed, 252 | # because we have git ;-) 253 | _UpgradeReport_Files/ 254 | Backup*/ 255 | UpgradeLog*.XML 256 | UpgradeLog*.htm 257 | ServiceFabricBackup/ 258 | *.rptproj.bak 259 | 260 | # SQL Server files 261 | *.mdf 262 | *.ldf 263 | *.ndf 264 | 265 | # Business Intelligence projects 266 | *.rdl.data 267 | *.bim.layout 268 | *.bim_*.settings 269 | *.rptproj.rsuser 270 | *- [Bb]ackup.rdl 271 | *- [Bb]ackup ([0-9]).rdl 272 | *- [Bb]ackup ([0-9][0-9]).rdl 273 | 274 | # Microsoft Fakes 275 | FakesAssemblies/ 276 | 277 | # GhostDoc plugin setting file 278 | *.GhostDoc.xml 279 | 280 | # Node.js Tools for Visual Studio 281 | .ntvs_analysis.dat 282 | node_modules/ 283 | 284 | # Visual Studio 6 build log 285 | *.plg 286 | 287 | # Visual Studio 6 workspace options file 288 | *.opt 289 | 290 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 291 | *.vbw 292 | 293 | # Visual Studio LightSwitch build output 294 | **/*.HTMLClient/GeneratedArtifacts 295 | **/*.DesktopClient/GeneratedArtifacts 296 | **/*.DesktopClient/ModelManifest.xml 297 | **/*.Server/GeneratedArtifacts 298 | **/*.Server/ModelManifest.xml 299 | _Pvt_Extensions 300 | 301 | # Paket dependency manager 302 | .paket/paket.exe 303 | paket-files/ 304 | 305 | # FAKE - F# Make 306 | .fake/ 307 | 308 | # CodeRush personal settings 309 | .cr/personal 310 | 311 | # Python Tools for Visual Studio (PTVS) 312 | __pycache__/ 313 | *.pyc 314 | 315 | # Cake - Uncomment if you are using it 316 | # tools/** 317 | # !tools/packages.config 318 | 319 | # Tabs Studio 320 | *.tss 321 | 322 | # Telerik's JustMock configuration file 323 | *.jmconfig 324 | 325 | # BizTalk build output 326 | *.btp.cs 327 | *.btm.cs 328 | *.odx.cs 329 | *.xsd.cs 330 | 331 | # OpenCover UI analysis results 332 | OpenCover/ 333 | 334 | # Azure Stream Analytics local run output 335 | ASALocalRun/ 336 | 337 | # MSBuild Binary and Structured Log 338 | *.binlog 339 | 340 | # NVidia Nsight GPU debugger configuration file 341 | *.nvuser 342 | 343 | # MFractors (Xamarin productivity tool) working folder 344 | .mfractor/ 345 | 346 | # Local History for Visual Studio 347 | .localhistory/ 348 | 349 | # BeatPulse healthcheck temp database 350 | healthchecksdb 351 | 352 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 353 | MigrationBackup/ 354 | 355 | # Ionide (cross platform F# VS Code tools) working folder 356 | .ionide/ 357 | 358 | #build path for CMake 359 | /src/build 360 | /build 361 | /runtimes 362 | /nupkgs 363 | /Models_Data.json 364 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 LSXPrime 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the “Software”), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /LibLoader.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibLoader", "src/LibLoader.csproj", "{3773AE5A-9A2B-49B2-AC5A-2C183D7806AF}" 4 | EndProject 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{92FC8A0C-D902-48C1-84AA-B456D137CC2D}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{16B9EB0E-5E53-42FC-BBF3-47DF70807C73}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibLoader.Samples.Basic", "samples/LibLoader.Samples.Basic\LibLoader.Samples.Basic.csproj", "{48A694DA-0D09-4D63-A276-3A244935A6E8}" 10 | EndProject 11 | Global 12 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 13 | Debug|Any CPU = Debug|Any CPU 14 | Release|Any CPU = Release|Any CPU 15 | EndGlobalSection 16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 17 | {3773AE5A-9A2B-49B2-AC5A-2C183D7806AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {3773AE5A-9A2B-49B2-AC5A-2C183D7806AF}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {3773AE5A-9A2B-49B2-AC5A-2C183D7806AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {3773AE5A-9A2B-49B2-AC5A-2C183D7806AF}.Release|Any CPU.Build.0 = Release|Any CPU 21 | {48A694DA-0D09-4D63-A276-3A244935A6E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {48A694DA-0D09-4D63-A276-3A244935A6E8}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {48A694DA-0D09-4D63-A276-3A244935A6E8}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {48A694DA-0D09-4D63-A276-3A244935A6E8}.Release|Any CPU.Build.0 = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(NestedProjects) = preSolution 27 | {3773AE5A-9A2B-49B2-AC5A-2C183D7806AF} = {92FC8A0C-D902-48C1-84AA-B456D137CC2D} 28 | {48A694DA-0D09-4D63-A276-3A244935A6E8} = {16B9EB0E-5E53-42FC-BBF3-47DF70807C73} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LibLoader: Cross-Platform Native Library Loader 2 | 3 | LibLoader is a powerful and versatile library designed to simplify the process of loading and interacting with native libraries (DLLs, SOs, dylibs, etc.) in C# applications. It provides a fluent API, cross-platform compatibility, dependency management, and various loading options, making it easier to integrate native code into your .NET projects. 4 | 5 | ## Features 6 | 7 | * **Cross-Platform Compatibility:** Leverages .NET's `NativeLibrary` for loading, supporting Windows, Linux, macOS, Android, and iOS. 8 | * **Dependency Management:** Automatically loads dependencies before the main library, handling versioning to ensure compatibility. 9 | * **Flexible Loading:** Load libraries from local files, embedded resources, or remote URLs with progress reporting and cancellation support. 10 | * **Progress Reporting:** Track download and loading progress using `IProgress`. 11 | * **Customizable Logging:** Control how and where log messages are written using a `LogDelegate`. 12 | * **Simplified Function Calling:** Invoke native functions with type safety and automatic marshaling using the intuitive `Call` method. 13 | * **Data Structure Marshaling:** Supports basic types, strings, structs (including nested structs and arrays of structs), and more. 14 | * **Delegate Caching:** Optimizes performance by caching delegate types, reducing overhead on repeated calls. 15 | * **Error Handling:** Provides clear and specific exceptions for various error conditions, such as missing libraries or functions. 16 | * **Fluent API:** Configure library loading using a clean and readable builder pattern, making configuration easy and maintainable. 17 | * **Singleton Instance:** Easily access the LibLoader instance via `LibLoader.Instance` for a streamlined workflow. 18 | * **Search Paths:** Specify additional search paths to locate native libraries, simplifying dependency management. 19 | * **Conditional Loading:** Load a library only if a condition is met, allowing for loading libraries based on runtime conditions. 20 | * **Implicit Loading:** Optionally enable implicit loading to avoid loading libraries on startup. Libraries will be loaded on the first call to a native function. 21 | 22 | ## Installation 23 | 24 | Install LibLoader using the NuGet Package Manager or the .NET CLI: 25 | 26 | ``` 27 | Install-Package LibLoader 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Basic Example: Loading a Local Library 33 | 34 | ```csharp 35 | using LibLoader; 36 | using System.Runtime.InteropServices; // For StructLayout 37 | 38 | // Define a struct matching the native library's data structure 39 | [StructLayout(LayoutKind.Sequential)] 40 | public struct MyData 41 | { 42 | public int Value; 43 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string Message; 44 | } 45 | 46 | public async Task Example() 47 | { 48 | var loader = LibLoader.Instance 49 | .ForPlatform(Platform.Windows, Bitness.X64) // Or your target platform and bitness 50 | .WithLibrary() 51 | .WithName("MyNativeLibrary", usePlatformSpecificName:true) // Library name (e.g., libMyNativeLibrary.so, MyNativeLibrary.dll) 52 | .FromCustomPath("./MyNativeLibrary.dll") // Path to the library file 53 | .WithNativeFunction() 54 | .Named("my_native_function") 55 | .WithReturnType() 56 | .WithParameter("input") 57 | .WithParameter("data") 58 | .Add() 59 | .Add() 60 | .Build(); 61 | 62 | await loader.LoadAsync(); 63 | 64 | var myData = new MyData { Value = 42, Message = "Some data" }; 65 | var result = loader.Call("MyNativeLibrary", "my_native_function", "Hello from C#", myData); 66 | 67 | Console.WriteLine($"Result from native function: {result}"); 68 | 69 | loader.Unload(); // Unload the library when finished 70 | } 71 | ``` 72 | 73 | 74 | 75 | ### Advanced Scenarios 76 | 77 | #### Loading from Embedded Resources 78 | 79 | ```csharp 80 | // ... within your LibLoader configuration ... 81 | .WithLibrary() 82 | .WithName("MyEmbeddedLibrary") 83 | .FromCustomPath("embedded:MyNamespace.MyEmbeddedLibrary.dll") // Path to the embedded resource. MyNamespace.MyEmbeddedLibrary.dll should exist as an Embedded Resource in your project 84 | // ... Add your native functions 85 | .Add(); 86 | ``` 87 | 88 | 89 | #### Loading from Remote URL with Progress and Cancellation 90 | 91 | ```csharp 92 | using System.Threading; 93 | 94 | // ... 95 | 96 | var cts = new CancellationTokenSource(); 97 | var progress = new Progress(p => Console.WriteLine($"Download progress: {p * 100:F2}%")); 98 | 99 | var loader = LibLoader.Instance 100 | .WithProgress(progress) 101 | .WithCancellationToken(cts.Token) // Pass the cancellation token 102 | .ForPlatform(Platform.Windows, Bitness.X64) 103 | .WithLibrary() 104 | .WithName("MyRemoteLibrary") 105 | .FromRemoteUrl("https://example.com/MyRemoteLibrary.zip", "MyRemoteLibrary.dll") // URL and filename (if inside an archive) 106 | // ... native functions 107 | .Add() 108 | .Build(); 109 | 110 | 111 | try 112 | { 113 | await loader.LoadAsync(); 114 | // ... call native functions ... 115 | } 116 | catch (OperationCanceledException) 117 | { 118 | Console.WriteLine("Library loading cancelled."); 119 | } 120 | finally 121 | { 122 | loader.Unload(); 123 | cts.Dispose(); // Dispose of cancellation token source 124 | } 125 | ``` 126 | 127 | 128 | #### Specifying Dependencies (with versioning) 129 | 130 | ```csharp 131 | // ... 132 | .WithDependency() // Define the dependencies themselves 133 | .WithName("DependencyA") 134 | .WithVersion("1.2.3") 135 | .FromCustomPath("./path/to/DependencyA.dll") // Or a remote URL, etc. 136 | .Add() 137 | .WithDependency() 138 | .WithName("DependencyB") 139 | .FromRemoteUrl("https://example.com/DependencyB.so") 140 | .Add(); 141 | .WithLibrary() 142 | .WithName("MyLibrary") 143 | .WithDependencies(("DependencyA", "1.2.3"), ("DependencyB", null)) // Dependency name and optional version 144 | // ... native functions 145 | .Add() 146 | ``` 147 | 148 | #### Conditional Loading (Platform-Specific Code) 149 | 150 | ```csharp 151 | .WithLibrary() 152 | .WithName("MyOptionalLibrary") 153 | .WithCondition(() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) // Load only on Windows 154 | // ... Windows-specific native functions ... 155 | .Add() 156 | .WithLibrary() 157 | .WithName("MyLinuxLibrary") 158 | .WithCondition(() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) // Load only on Linux 159 | // ... Linux-specific native functions ... 160 | .Add(); 161 | ``` 162 | 163 | #### Custom Data Structures (Nested Structs and Arrays) 164 | 165 | ```csharp 166 | [StructLayout(LayoutKind.Sequential)] 167 | public struct InnerData 168 | { 169 | public int Id; 170 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string Name; 171 | } 172 | 173 | [StructLayout(LayoutKind.Sequential)] 174 | public struct MyData 175 | { 176 | public int Value; 177 | public InnerData Inner; 178 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] public int[] ArrayData; 179 | } 180 | 181 | 182 | // ... within the WithLibrary() builder 183 | .WithNativeFunction() 184 | .Named("process_complex_data") 185 | .WithReturnType() 186 | .WithParameter("data") 187 | .Add() 188 | // ... other native functions ... 189 | .Add(); 190 | 191 | // ... After loading the library 192 | var data = new MyData 193 | { 194 | Value = 123, 195 | Inner = new InnerData { Id = 456, Name = "Nested" }, 196 | ArrayData = new int[] { 1, 2, 3, 4, 5 } 197 | }; 198 | 199 | var result = loader.Call("MyLibrary", "process_complex_data", data); 200 | Console.WriteLine($"Result: {result}"); 201 | 202 | ``` 203 | 204 | 205 | #### Implicit Loading (Automatic Loading on First Call) 206 | 207 | 208 | ```csharp 209 | // Enable implicit loading to avoid explicitly calling LoadAsync() 210 | var loader = LibLoader.Instance.WithImplicitLoading(); 211 | // ... the rest of your LibLoader configuration (platforms, libraries, functions) 212 | 213 | // The libraries are loaded automatically when a native function is called: 214 | 215 | var result = loader.Call("MyNativeLibrary", "my_function", 10); 216 | // ... 217 | ``` 218 | 219 | #### Search Paths (For Dependencies or Non-Standard Locations) 220 | 221 | ```csharp 222 | // Add search paths for native libraries 223 | var searchPaths = new List(); 224 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 225 | { 226 | searchPaths.Add(@"C:\Path\To\My\Libraries"); 227 | } 228 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 229 | { 230 | searchPaths.Add("/usr/local/lib"); 231 | } 232 | 233 | 234 | var loader = LibLoader.Instance.WithSearchPaths(searchPaths); 235 | 236 | // ... rest of the LibLoader configuration 237 | ``` 238 | 239 | ## API Documentation 240 | 241 | Full API documentation is available in the code comments and through IntelliSense within your IDE. Here's a brief overview: 242 | 243 | * **`LibLoader.Instance`:** The singleton instance of the `LibLoader` class. 244 | * **`ForPlatform(Platform platform, Bitness bitness)`:** Specifies the target platform and bitness (x86, x64, Arm, Arm64). 245 | * **`WithLibrary()`:** Starts a builder for defining a native library. 246 | * **`WithName(string libraryName)`:** Sets the name of the native library (without extension). 247 | * **`FromCustomPath(string path)`:** Loads the library from a specific file path. 248 | * **`FromRemoteUrl(string url, string? filename)`:** Downloads and loads the library from a remote URL. `filename` is optional and specifies the filename within a zip archive. 249 | * **`WithDependencies(params (string name, string version)[] dependencies)`:** Specifies dependencies for a library. 250 | * **`WithCondition(Func condition)`:** Loads the library only if the condition is true. 251 | * **`WithNativeFunction()`:** Starts a builder for defining a native function. 252 | * **`Named(string nativeName)`:** Sets the name of the native function. 253 | * **`WithCallingName(string callingName)`:** Specifies the C# name for the function (optional, defaults to the native name). 254 | * **`WithReturnType()` / `WithReturnType(Type returnType)`:** Sets the return type of the function. 255 | * **`WithParameter(string name)`:** Adds a parameter to the function signature. 256 | * **`Add()`:** Adds the defined library or function to the loader. 257 | * **`Build()`:** Finalizes the configuration and returns the `LibLoader` instance. 258 | * **`LoadAsync()`:** Asynchronously loads the configured libraries. (Not necessary with implicit loading.) 259 | * **`Call(string libraryName, string functionName, params object[] arguments)`:** Calls a native function with the specified name and arguments, returning a value of type T. Overloads exist for `void` return types and to specify the return type using a `Type` object. 260 | * **`Unload()`:** Unloads all loaded libraries. 261 | 262 | 263 | ## Diagram/Architecture 264 | 265 | ``` 266 | +---------------------+ +---------------------+ +---------------------+ 267 | | Application | ====> | LibLoader | ====> | Native Library | 268 | +---------------------+ +---------------------+ +---------------------+ 269 | (1) Configure (2) Load & Call 270 | Functions 271 | ``` 272 | 273 | 274 | 1. **Configure:** The application configures `LibLoader` with information about the native libraries to load (name, location, dependencies, functions). 275 | 2. **Load & Call Functions:** `LibLoader` loads the specified native libraries and provides a way for the application to call functions within those libraries. It handles dependency resolution, platform-specific loading, and data marshaling. 276 | 277 | 278 | ## Error Handling 279 | 280 | LibLoader throws the following exceptions: 281 | 282 | * **`MissingLibraryException`:** Thrown when a required native library cannot be found. 283 | * **`MissingFunctionException`:** Thrown when a native function cannot be found within a loaded library. 284 | * **`LibraryLoadException`:** Thrown when an error occurs while loading a native library (e.g., invalid format, dependencies not met). 285 | * **`NativeFunctionCallException`:** Thrown when an error occurs during a native function call (e.g., incorrect arguments, access violation). 286 | * `UnsupportedPlatformException`: Thrown when the current platform is not supported. 287 | * `UnsupportedArchitectureException`: Thrown when the current process architecture is not supported. 288 | 289 | ## Contributing 290 | 291 | Contributions are welcome! Please follow these guidelines: 292 | 293 | * Fork the repository. 294 | * Create a new branch for your feature or bug fix. 295 | * Write clear and concise code with comments. 296 | * Submit a pull request. 297 | 298 | ## License 299 | 300 | This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details. 301 | -------------------------------------------------------------------------------- /samples/LibLoader.Samples.Basic/LibLoader.Samples.Basic.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Always 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /samples/LibLoader.Samples.Basic/LocalFileServer.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Sockets; 3 | using System.Reflection; 4 | 5 | namespace LibLoader.Samples.Basic; 6 | 7 | public class LocalFileServer(string resourceName, int speedLimitKbps) 8 | { 9 | public int Port { get; private set; } 10 | private readonly int _speedLimitBytesPerSecond = speedLimitKbps * 1024 / 8; 11 | private bool _isRunning; 12 | private TcpListener? _listener; 13 | 14 | public async Task StartAsync() 15 | { 16 | _listener = new TcpListener(IPAddress.Loopback, 0); 17 | _listener.Start(); 18 | 19 | _isRunning = true; 20 | Port = ((IPEndPoint)_listener.LocalEndpoint).Port; 21 | Console.WriteLine($"Server listening on port {Port}"); 22 | 23 | while (_isRunning) 24 | { 25 | try 26 | { 27 | var client = await _listener.AcceptTcpClientAsync(); 28 | Console.WriteLine("Client connected."); 29 | _ = HandleClientAsync(client); // Handle client in the background 30 | } 31 | catch (Exception ex) 32 | { 33 | Console.WriteLine($"Error accepting client: {ex.Message}"); 34 | } 35 | } 36 | } 37 | 38 | private async Task HandleClientAsync(TcpClient client) 39 | { 40 | try 41 | { 42 | await using var stream = client.GetStream(); 43 | await using var writer = new StreamWriter(stream); 44 | // Accessing embedded resource 45 | var assembly = Assembly.GetExecutingAssembly(); 46 | await using var resourceStream = assembly.GetManifestResourceStream(resourceName); 47 | if (resourceStream == null) 48 | { 49 | await writer.WriteLineAsync($"HTTP/1.1 404 Not Found"); 50 | await writer.WriteLineAsync("Content-Length: 0"); 51 | await writer.WriteLineAsync("Connection: close"); 52 | await writer.WriteLineAsync(""); 53 | await writer.FlushAsync(); 54 | 55 | return; 56 | } 57 | 58 | // Send HTTP headers 59 | await writer.WriteLineAsync("HTTP/1.1 200 OK"); 60 | await writer.WriteLineAsync($"Content-Length: {resourceStream.Length}"); 61 | await writer.WriteLineAsync("Content-Type: application/octet-stream"); // Or the correct MIME type 62 | await writer.WriteLineAsync(""); // Empty line to signify end of headers 63 | await writer.FlushAsync(); 64 | 65 | var buffer = new byte[4096]; 66 | int bytesRead; 67 | 68 | while ((bytesRead = await resourceStream.ReadAsync(buffer)) > 0) 69 | { 70 | var startTime = DateTime.Now.Ticks; 71 | await stream.WriteAsync(buffer.AsMemory(0, bytesRead)); 72 | 73 | var elapsedTicks = DateTime.Now.Ticks - startTime; 74 | var elapsedSeconds = elapsedTicks / (double)TimeSpan.TicksPerSecond; 75 | 76 | if (_speedLimitBytesPerSecond > 0) 77 | { 78 | var expectedTransferTime = (double)bytesRead / _speedLimitBytesPerSecond; 79 | if (elapsedSeconds < expectedTransferTime) 80 | { 81 | var sleepMilliseconds = (int)((expectedTransferTime - elapsedSeconds) * 1000); 82 | if (sleepMilliseconds > 0) 83 | await Task.Delay(sleepMilliseconds); 84 | } 85 | } 86 | } 87 | 88 | Console.WriteLine("File transfer complete."); 89 | } 90 | catch (Exception ex) 91 | { 92 | Console.WriteLine($"Error handling client: {ex.Message}"); 93 | } 94 | finally 95 | { 96 | client.Close(); 97 | } 98 | } 99 | 100 | 101 | public void Stop() 102 | { 103 | _isRunning = false; 104 | _listener?.Stop(); 105 | } 106 | } -------------------------------------------------------------------------------- /samples/LibLoader.Samples.Basic/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace LibLoader.Samples.Basic; 4 | 5 | internal static class Program 6 | { 7 | private static async Task Main() 8 | { 9 | Console.WriteLine("1 - Load a library from a local file with dependencies"); 10 | Console.WriteLine("2 - Load a library from embedded resources"); 11 | Console.WriteLine("3 - Load a library from a remote URL with progress reporting"); 12 | Console.WriteLine("4 - Load a library from a local file & Test custom data structures"); 13 | Console.WriteLine("Choose a scenario (1-3):"); 14 | var scenario = int.Parse(Console.ReadKey().KeyChar.ToString()); 15 | 16 | switch (scenario) 17 | { 18 | case 1: 19 | await LoadLibraryFromLocalFile(); 20 | break; 21 | case 2: 22 | await LoadLibraryFromEmbeddedResources(); 23 | break; 24 | case 3: 25 | await LoadLibraryFromRemoteUrl(); 26 | break; 27 | case 4: 28 | await TestCustomDataStructures(); 29 | break; 30 | default: 31 | Console.WriteLine("Invalid scenario selected."); 32 | break; 33 | } 34 | 35 | 36 | Console.ReadKey(); 37 | } 38 | 39 | 40 | 41 | 42 | private static async Task LoadLibraryFromLocalFile() 43 | { 44 | // Example 1: Loading a library from a local file with dependencies 45 | var loader = LibLoader.Instance 46 | .WithTargetDirectory(string.Empty) 47 | .WithImplicitLoading() 48 | .ForPlatform(Platform.Windows, Bitness.X64) 49 | .WithLibrary() 50 | .WithName("libMyNativeLib", usePlatformSpecificName: true) // Use platform-specific naming 51 | .FromCustomPath("./libMyNativeLib.dll") // Path to your local library file 52 | .WithDependencies(("DependencyLib", "0.0")) 53 | .WithNativeFunction() 54 | .Named("my_function") // Native function name 55 | .WithCallingName("MyFunction") // Optional C# calling name 56 | .WithReturnType() 57 | .WithParameter("value") 58 | .Add() 59 | .Add() 60 | .WithDependency() 61 | .WithName("DependencyLib") 62 | .FromCustomPath("./DependencyLib.dll") 63 | .Add() 64 | .Build(); 65 | 66 | await loader.LoadAsync(); 67 | var result = loader.Call("libMyNativeLib", "MyFunction", 10); 68 | Console.WriteLine($"Result from MyFunction: {result}"); 69 | loader.Unload(); 70 | } 71 | 72 | private static async Task LoadLibraryFromEmbeddedResources() 73 | { 74 | // Example 2: Loading a library from embedded resources 75 | var resourceLoader = LibLoader.Instance 76 | .ForPlatform(Platform.Windows, Bitness.X64) 77 | .WithLibrary() 78 | .WithName("libMyNativeLib", usePlatformSpecificName: true) 79 | .FromCustomPath("embedded:LibLoader.Samples.Basic.libMyNativeLib.dll") // Path to embedded resource 80 | .WithNativeFunction() 81 | .Named("embedded_function") 82 | .WithCallingName("EmbeddedFunction") 83 | .WithReturnType(typeof(void)) 84 | .Add() 85 | .Add() 86 | .Build(); 87 | 88 | await resourceLoader.LoadAsync(); 89 | resourceLoader.Call(typeof(void), "libMyNativeLib", "EmbeddedFunction"); 90 | Console.WriteLine("Embedded function called successfully."); 91 | resourceLoader.Unload(); 92 | } 93 | 94 | private static async Task LoadLibraryFromRemoteUrl() 95 | { 96 | // Example 3: Loading from a remote URL with progress reporting 97 | var localServer = new LocalFileServer("LibLoader.Samples.Basic.libMyNativeLib.dll", 75); 98 | _ = localServer.StartAsync(); 99 | var progress = new Progress(p => Console.WriteLine($"Download Progress: {p * 100:F2}%")); 100 | var remoteLoader = LibLoader.Instance 101 | .WithProgress(progress) 102 | .ForPlatform(Platform.Windows, Bitness.X64) 103 | .WithLibrary() 104 | .WithName("libMyNativeLib", usePlatformSpecificName: true) 105 | .FromRemoteUrl($"http://127.0.0.1:{localServer.Port}", "libMyNativeLib.dll") 106 | .WithNativeFunction() 107 | .Named("remote_function") 108 | .WithCallingName("RemoteFunction") 109 | .WithReturnType() 110 | .Add() 111 | .Add() 112 | .Build(); 113 | 114 | await remoteLoader.LoadAsync(); 115 | var remoteResult = remoteLoader.Call("libMyNativeLib", "RemoteFunction"); 116 | Console.WriteLine($"Remote Function Result: {remoteResult}"); 117 | 118 | remoteLoader.Unload(); 119 | localServer.Stop(); 120 | } 121 | 122 | private static async Task TestCustomDataStructures() 123 | { 124 | // Example 4: Loading a library from a local file & Test custom data structures 125 | var loader = LibLoader.Instance 126 | .WithTargetDirectory(string.Empty) 127 | .WithImplicitLoading() 128 | .ForPlatform(Platform.Windows, Bitness.X64) 129 | .WithLibrary() 130 | .WithName("libMyNativeLib") 131 | .FromCustomPath("./libMyNativeLib.dll") 132 | .WithNativeFunction() 133 | .Named("get_data") 134 | .WithCallingName("GetData") 135 | .WithReturnType() 136 | .Add() 137 | .WithNativeFunction() 138 | .Named("process_data") 139 | .WithCallingName("ProcessData") 140 | .WithReturnType(typeof(void)) 141 | .WithParameter("data") 142 | .Add() 143 | .WithNativeFunction() 144 | .Named("modify_data") 145 | .WithCallingName("ModifyData") 146 | .WithReturnType() 147 | .WithParameter("data") 148 | .Add() 149 | // Test nested structs 150 | .WithNativeFunction() 151 | .Named("get_nested_data") 152 | .WithCallingName("GetNestedData") 153 | .WithReturnType() 154 | .Add() 155 | // Test arrays of structs 156 | .WithNativeFunction() 157 | .Named("get_array_data") 158 | .WithCallingName("GetArrayData") 159 | .WithReturnType() 160 | .Add() 161 | .WithNativeFunction() 162 | .Named("process_array_data") 163 | .WithCallingName("ProcessArrayData") 164 | .WithReturnType(typeof(void)) 165 | .WithParameter("data") 166 | .Add() 167 | .Add() 168 | .Build(); 169 | 170 | await loader.LoadAsync(); 171 | 172 | 173 | var data = loader.Call("libMyNativeLib", "GetData"); 174 | Console.WriteLine($"Received data: Value = {data.value}, Message = {data.message}"); 175 | 176 | 177 | var modifiedData = loader.Call("libMyNativeLib", "ModifyData", data); 178 | Console.WriteLine($"Modified data: Value = {modifiedData.value}, Message = {modifiedData.message}"); 179 | 180 | // Test processing data 181 | loader.Call("libMyNativeLib", "ProcessData", data); 182 | 183 | // Nested Structs test: 184 | var nestedData = loader.Call("libMyNativeLib", "GetNestedData"); 185 | Console.WriteLine($"Nested Data: Id={nestedData.id}, Name={nestedData.name}, " + 186 | $"InnerValue={nestedData.inner_data.value}, InnerMessage={nestedData.inner_data.message}"); 187 | 188 | // Array of Structs test 189 | var arrayData = loader.Call("libMyNativeLib", "GetArrayData"); 190 | Console.WriteLine($"Array Data Count: {arrayData.count}"); 191 | for (var i = 0; i < arrayData.count; i++) 192 | { 193 | Console.WriteLine($" Data[{i}]: Value = {arrayData.data[i].value}, Message = {arrayData.data[i].message}"); 194 | } 195 | 196 | loader.Call("libMyNativeLib", "ProcessArrayData", arrayData); 197 | 198 | loader.Unload(); 199 | } 200 | 201 | } 202 | 203 | 204 | // Define the MyData struct to match the C definition 205 | public struct MyData 206 | { 207 | public int value; 208 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] // Important for string marshaling 209 | public string message; 210 | } 211 | 212 | public struct NestedData 213 | { 214 | public int id; 215 | 216 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] 217 | public string name; 218 | public MyData inner_data; 219 | } 220 | 221 | 222 | public struct ArrayData 223 | { 224 | public int count; 225 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] 226 | public MyData[] data; 227 | } 228 | 229 | 230 | 231 | // The C code for the MyNativeLib library that used in the sample 232 | /* 233 | #include 234 | #include 235 | #include 236 | 237 | // Custom data structure 238 | typedef struct MyData { 239 | int value; 240 | char message[256]; // Fixed size buffer to match C# marshaling 241 | } MyData; 242 | 243 | 244 | // Nested struct example 245 | typedef struct NestedData { 246 | int id; 247 | char name[64]; 248 | MyData inner_data; 249 | } NestedData; 250 | 251 | 252 | // Array example 253 | typedef struct ArrayData { 254 | int count; 255 | MyData data[10]; // Fixed size array 256 | } ArrayData; 257 | 258 | 259 | // MyNativeLib functions 260 | __declspec(dllexport) int my_function(int value) { 261 | printf("Hello from MyNativeLib! Value received: %d\n", value); 262 | return value * 2; 263 | } 264 | 265 | // DependencyLib functions 266 | __declspec(dllexport) void dependency_function() { 267 | printf("Hello from DependencyLib!\n"); 268 | } 269 | 270 | // EmbeddedLib functions 271 | __declspec(dllexport) void embedded_function() { 272 | printf("Hello from EmbeddedLib!\n"); 273 | } 274 | 275 | // RemoteLib functions 276 | __declspec(dllexport) const char *remote_function() { 277 | printf("Hello from RemoteLib!\n"); 278 | return "Message from remote library"; 279 | } 280 | 281 | // New functions with custom data structures 282 | __declspec(dllexport) MyData get_data() { 283 | MyData data; 284 | data.value = 42; 285 | strncpy_s(data.message, sizeof(data.message), "Data from get_data()", _TRUNCATE); 286 | printf("get_data() called\n"); 287 | return data; 288 | } 289 | 290 | __declspec(dllexport) void process_data(const MyData data) { 291 | printf("process_data() called. Value: %d, Message: %s\n", data.value, data.message); 292 | } 293 | 294 | __declspec(dllexport) MyData modify_data(const MyData data) { 295 | MyData modified_data; 296 | modified_data.value = data.value * 2; 297 | 298 | strcat_s(modified_data.message, sizeof(modified_data.message), " (modified)"); 299 | 300 | 301 | printf("modify_data() called\n"); 302 | return modified_data; 303 | } 304 | 305 | 306 | __declspec(dllexport) NestedData get_nested_data() { 307 | NestedData nested; 308 | nested.id = 1; 309 | strncpy_s(nested.name, sizeof(nested.name), "Nested Data", _TRUNCATE); 310 | nested.inner_data = get_data(); 311 | printf("get_nested_data() called\n"); 312 | return nested; 313 | } 314 | 315 | __declspec(dllexport) ArrayData get_array_data() { 316 | ArrayData array; 317 | array.count = 3; 318 | for (int i = 0; i < array.count; i++) { 319 | array.data[i] = get_data(); 320 | array.data[i].value += i; 321 | } 322 | printf("get_array_data() called\n"); 323 | return array; 324 | } 325 | 326 | // Function to process an array of MyData 327 | __declspec(dllexport) void process_array_data(const ArrayData data) { 328 | printf("process_array_data() called\n"); 329 | for (int i = 0; i < data.count; i++) { 330 | printf(" Data[%d]: Value = %d, Message = %s\n", i, data.data[i].value, data.data[i].message); 331 | } 332 | } 333 | */ -------------------------------------------------------------------------------- /samples/LibLoader.Samples.Basic/libMyNativeLib.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSXPrime/LibLoader/ede509e32fd47287778b49824a529dc08180a565/samples/LibLoader.Samples.Basic/libMyNativeLib.dll -------------------------------------------------------------------------------- /src/Builders/DependencyDefinitionBuilder.cs: -------------------------------------------------------------------------------- 1 | using LibLoader.Interfaces; 2 | using LibLoader.Models; 3 | 4 | namespace LibLoader.Builders; 5 | 6 | /// 7 | /// Builds a definition for a dependency library for a specific platform and bitness. 8 | /// 9 | public class DependencyDefinitionBuilder(IPlatformBuilder platformBuilder, Platform platform, Bitness bitness) 10 | : ILibraryDefinitionBuilder 11 | { 12 | private string _libraryName = string.Empty; 13 | private string? _version; 14 | private string? _customPath; 15 | private Func? _condition; 16 | private string? _remoteUrl; 17 | private string? _remoteFileName; 18 | 19 | /// 20 | /// Sets the name of the dependency library. 21 | /// 22 | /// The name of the library. 23 | /// If true, automatically converts the name to the platform-specific format. 24 | /// The DependencyDefinitionBuilder instance for chaining method calls. 25 | public ILibraryDefinitionBuilder WithName(string libraryName, bool usePlatformSpecificName = false) 26 | { 27 | _libraryName = usePlatformSpecificName ? LibLoader.GetPlatformSpecificName(libraryName, platform) : libraryName; 28 | return this; 29 | } 30 | 31 | /// 32 | /// Sets the version of the dependency library. 33 | /// 34 | /// The version of the library (optional). 35 | /// The DependencyDefinitionBuilder instance for chaining method calls. 36 | public ILibraryDefinitionBuilder WithVersion(string? version) 37 | { 38 | _version = version; 39 | return this; 40 | } 41 | 42 | /// 43 | /// Sets the custom path to the dependency library. 44 | /// 45 | /// The custom path to the library. 46 | /// The DependencyDefinitionBuilder instance for chaining method calls. 47 | public ILibraryDefinitionBuilder FromCustomPath(string? customPath) 48 | { 49 | _customPath = customPath; 50 | return this; 51 | } 52 | 53 | /// 54 | /// **NOT SUPPORTED**: Sets the dependencies of the dependency library. 55 | /// 56 | /// An array of (name, version) tuples representing the dependencies. 57 | /// The DependencyDefinitionBuilder instance for chaining method calls. 58 | /// 59 | /// Dependencies cannot depend on other dependencies at this time. This method will throw a . 60 | /// 61 | public ILibraryDefinitionBuilder WithDependencies(params (string name, string version)[] dependencies) 62 | { 63 | throw new NotImplementedException("Dependencies can't depend on other dependencies yet."); 64 | } 65 | 66 | /// 67 | /// Sets a condition function that determines whether to load this dependency library. 68 | /// 69 | /// The condition function. 70 | /// The DependencyDefinitionBuilder instance for chaining method calls. 71 | public ILibraryDefinitionBuilder WithCondition(Func? condition) 72 | { 73 | _condition = condition; 74 | return this; 75 | } 76 | 77 | /// 78 | /// **NOT SUPPORTED**: Adds a native function to the dependency library definition. 79 | /// 80 | /// A native function builder instance. 81 | /// 82 | /// Higher-level implementations should not call lower-level dependencies. This method will throw a . 83 | /// 84 | public INativeFunctionBuilder WithNativeFunction() 85 | { 86 | throw new NotImplementedException("Higher level implementation shouldn't call lower level dependencies."); 87 | } 88 | 89 | /// 90 | /// Sets the remote URL from which to download the dependency library. 91 | /// 92 | /// The remote URL. 93 | /// The file name of the library in the remote location (optional). 94 | /// The DependencyDefinitionBuilder instance for chaining method calls. 95 | public ILibraryDefinitionBuilder FromRemoteUrl(string? remoteUrl, string? remoteFileName = null) 96 | { 97 | _remoteUrl = remoteUrl; 98 | _remoteFileName = remoteFileName; 99 | return this; 100 | } 101 | 102 | /// 103 | /// Adds the dependency library definition to the LibLoader instance. 104 | /// 105 | /// The platform builder instance for chaining method calls. 106 | public IPlatformBuilder Add() 107 | { 108 | if (!LibLoader.Instance.Dependencies.ContainsKey(Tuple.Create(platform, bitness))) 109 | LibLoader.Instance.Dependencies.Add(Tuple.Create(platform, bitness), []); 110 | 111 | var parsedVersion = Version.Parse("0.0.0"); 112 | if (!string.IsNullOrEmpty(_version)) 113 | if (!Version.TryParse(_version, out parsedVersion)) 114 | throw new ArgumentException($"Invalid version string: {_version}"); 115 | 116 | var platformSpecificName = LibLoader.GetPlatformSpecificName(_libraryName, platform); 117 | LibLoader.Instance.Dependencies[Tuple.Create(platform, bitness)].Add(new LibraryDefinition 118 | { 119 | LibraryName = platformSpecificName, 120 | Version = parsedVersion, 121 | CustomPath = _customPath, 122 | RemoteUrl = _remoteUrl, 123 | RemoteFileName = _remoteFileName, 124 | Condition = _condition 125 | }); 126 | 127 | return platformBuilder; 128 | } 129 | } -------------------------------------------------------------------------------- /src/Builders/LibraryDefinitionBuilder.cs: -------------------------------------------------------------------------------- 1 | using LibLoader.Interfaces; 2 | using LibLoader.Models; 3 | 4 | namespace LibLoader.Builders; 5 | 6 | /// 7 | /// Builds a definition for a native library for a specific platform. 8 | /// 9 | public class LibraryDefinitionBuilder(IPlatformBuilder platformBuilder, Platform platform) 10 | : ILibraryDefinitionBuilder 11 | { 12 | private readonly LibraryDefinition _libraryDefinition = new(); 13 | 14 | /// 15 | /// Sets the name of the library. 16 | /// 17 | /// The name of the library. 18 | /// If true, automatically converts the name to the platform-specific format. 19 | /// The LibraryDefinitionBuilder instance for chaining method calls. 20 | public ILibraryDefinitionBuilder WithName(string libraryName, bool usePlatformSpecificName = true) 21 | { 22 | _libraryDefinition.LibraryName = usePlatformSpecificName ? LibLoader.GetPlatformSpecificName(libraryName, platform) : libraryName; 23 | return this; 24 | } 25 | 26 | /// 27 | /// Sets the version of the library. 28 | /// 29 | /// The version of the library (optional). 30 | /// The LibraryDefinitionBuilder instance for chaining method calls. 31 | public ILibraryDefinitionBuilder WithVersion(string? version) 32 | { 33 | if (!string.IsNullOrEmpty(version) && Version.TryParse(version, out var parsedVersion)) 34 | { 35 | _libraryDefinition.Version = parsedVersion; 36 | } 37 | return this; 38 | } 39 | 40 | /// 41 | /// Sets the custom path to the library. 42 | /// 43 | /// The custom path to the library. 44 | /// The LibraryDefinitionBuilder instance for chaining method calls. 45 | public ILibraryDefinitionBuilder FromCustomPath(string? customPath) 46 | { 47 | _libraryDefinition.CustomPath = customPath; 48 | return this; 49 | } 50 | 51 | /// 52 | /// Sets the remote URL from which to download the library. 53 | /// 54 | /// The remote URL. 55 | /// The file name of the library in the remote location (optional). 56 | /// The LibraryDefinitionBuilder instance for chaining method calls. 57 | public ILibraryDefinitionBuilder FromRemoteUrl(string? remoteUrl, string? remoteFileName = null) 58 | { 59 | _libraryDefinition.RemoteUrl = remoteUrl; 60 | _libraryDefinition.RemoteFileName = remoteFileName; 61 | return this; 62 | } 63 | 64 | /// 65 | /// Sets the dependencies of the library. 66 | /// 67 | /// An array of (name, version) tuples representing the dependencies. 68 | /// The LibraryDefinitionBuilder instance for chaining method calls. 69 | public ILibraryDefinitionBuilder WithDependencies(params (string name, string version)[] dependencies) 70 | { 71 | _libraryDefinition.Dependencies = dependencies; 72 | return this; 73 | } 74 | 75 | /// 76 | /// Sets a condition function that determines whether to load this library. 77 | /// 78 | /// The condition function. 79 | /// The LibraryDefinitionBuilder instance for chaining method calls. 80 | public ILibraryDefinitionBuilder WithCondition(Func? condition) 81 | { 82 | _libraryDefinition.Condition = condition; 83 | return this; 84 | } 85 | 86 | /// 87 | /// Starts building a native function definition for the library. 88 | /// 89 | /// A native function builder instance. 90 | public INativeFunctionBuilder WithNativeFunction() 91 | { 92 | return new NativeFunctionBuilder(this, _libraryDefinition); 93 | } 94 | 95 | /// 96 | /// Adds the library definition to the LibLoader instance. 97 | /// 98 | /// The platform builder instance for chaining method calls. 99 | public IPlatformBuilder Add() 100 | { 101 | LibLoader.Instance.Libraries.Add(_libraryDefinition); 102 | return platformBuilder; 103 | } 104 | } -------------------------------------------------------------------------------- /src/Builders/NativeFunctionDefinitionBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Runtime.InteropServices; 3 | using LibLoader.Interfaces; 4 | using LibLoader.Models; 5 | using LibLoader.Native; 6 | 7 | namespace LibLoader.Builders; 8 | 9 | /// 10 | /// Builds a definition for a native function within a library. 11 | /// 12 | public class NativeFunctionBuilder(ILibraryDefinitionBuilder builder, LibraryDefinition library) : INativeFunctionBuilder 13 | { 14 | private string _nativeName = string.Empty; 15 | private string _callingName = string.Empty; 16 | private CallingConvention _callingConvention = CallingConvention.Cdecl; 17 | private Type _returnType = typeof(void); 18 | private readonly List _parameters = []; 19 | 20 | /// 21 | /// Sets the native name of the function. 22 | /// 23 | /// The native name of the function. 24 | /// The NativeFunctionBuilder instance for chaining method calls. 25 | public INativeFunctionBuilder Named(string nativeName) 26 | { 27 | _nativeName = nativeName; 28 | return this; 29 | } 30 | 31 | /// 32 | /// Sets the calling name of the function (used in managed code). 33 | /// 34 | /// The calling name of the function. 35 | /// The NativeFunctionBuilder instance for chaining method calls. 36 | public INativeFunctionBuilder WithCallingName(string callingName) 37 | { 38 | _callingName = callingName; 39 | return this; 40 | } 41 | 42 | /// 43 | /// Sets the calling convention used for the function. 44 | /// 45 | /// The calling convention, defaults to . 46 | /// The NativeFunctionBuilder instance for chaining method calls. 47 | public INativeFunctionBuilder WithCallingConvention(CallingConvention callingConvention) 48 | { 49 | _callingConvention = callingConvention; 50 | return this; 51 | } 52 | 53 | /// 54 | /// Sets the return type of the function using a generic type parameter. 55 | /// 56 | /// The return type of the function. 57 | /// The NativeFunctionBuilder instance for chaining method calls. 58 | public INativeFunctionBuilder WithReturnType() 59 | { 60 | _returnType = typeof(T); 61 | return this; 62 | } 63 | 64 | /// 65 | /// Sets the return type of the function using a Type object. 66 | /// 67 | /// The return type of the function. 68 | /// The NativeFunctionBuilder instance for chaining method calls. 69 | public INativeFunctionBuilder WithReturnType(Type returnType) 70 | { 71 | _returnType = returnType; 72 | return this; 73 | } 74 | 75 | /// 76 | /// Adds a parameter to the function using a generic type parameter. 77 | /// 78 | /// The type of the parameter. 79 | /// The name of the parameter. 80 | /// The NativeFunctionBuilder instance for chaining method calls. 81 | public INativeFunctionBuilder WithParameter(string name) 82 | { 83 | _parameters.Add(new NativeFunctionParameter { Name = name, Type = typeof(T) }); 84 | return this; 85 | } 86 | 87 | /// 88 | /// Adds the native function definition to the library. 89 | /// 90 | /// The library definition builder instance for chaining method calls. 91 | public ILibraryDefinitionBuilder Add() 92 | { 93 | if (string.IsNullOrEmpty(_nativeName)) 94 | throw new InvalidOperationException("Native function name cannot be empty."); 95 | 96 | if (string.IsNullOrEmpty(_callingName)) 97 | _callingName = _nativeName; 98 | 99 | if (library == null) 100 | throw new InvalidOperationException( 101 | $"Cannot add native function '{_nativeName}' to the library: Library '{library}' was not found in the LibLoader."); 102 | 103 | // Create the function definition 104 | var functionDefinition = new NativeFunctionDefinition 105 | { 106 | Library = library, 107 | CallingName = _callingName, 108 | NativeName = _nativeName, 109 | CallingConvention = _callingConvention, 110 | ReturnType = _returnType, 111 | Parameters = _parameters, 112 | }; 113 | 114 | // Create a wrapper method that matches the expected delegate signature 115 | var parameterTypes = _parameters.Select(p => p.Type).ToArray(); 116 | var delegateType = Expression.GetDelegateType(parameterTypes.Concat(new[] { _returnType }).ToArray()); 117 | 118 | // Create wrapper delegate that will forward the call to InternalCall 119 | functionDefinition.Delegate = DelegateFactory.CreateWrapperDelegate(functionDefinition, delegateType); 120 | 121 | // Store the function definition in the library 122 | library.NativeFunctions.Add(functionDefinition.NativeName, functionDefinition); 123 | if (!string.IsNullOrEmpty(functionDefinition.CallingName) && functionDefinition.CallingName != functionDefinition.NativeName) 124 | library.NativeFunctions.Add(functionDefinition.CallingName, functionDefinition); 125 | 126 | return builder; 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /src/Builders/PlatformBuilder.cs: -------------------------------------------------------------------------------- 1 | using LibLoader.Interfaces; 2 | 3 | namespace LibLoader.Builders; 4 | 5 | /// 6 | /// Builds definitions for native libraries and dependencies for a specific platform and bitness. 7 | /// 8 | public class PlatformBuilder(Platform platform, Bitness bitness) : IPlatformBuilder 9 | { 10 | /// 11 | /// Starts building a definition for a native library. 12 | /// 13 | /// A library definition builder instance. 14 | public ILibraryDefinitionBuilder WithLibrary() => new LibraryDefinitionBuilder(this, platform); 15 | 16 | /// 17 | /// Starts building a definition for a dependency library. 18 | /// 19 | /// A dependency definition builder instance. 20 | public ILibraryDefinitionBuilder WithDependency() => new DependencyDefinitionBuilder(this, platform, bitness); 21 | 22 | /// 23 | /// Builds the LibLoader instance with the defined libraries and dependencies. 24 | /// 25 | /// The LibLoader instance. 26 | public LibLoader Build() => LibLoader.Instance; 27 | } -------------------------------------------------------------------------------- /src/Exceptions/LibraryLoadException.cs: -------------------------------------------------------------------------------- 1 | namespace LibLoader.Exceptions; 2 | 3 | public class LibraryLoadException(string message) : Exception(message); -------------------------------------------------------------------------------- /src/Exceptions/MissingFunctionException.cs: -------------------------------------------------------------------------------- 1 | namespace LibLoader.Exceptions; 2 | 3 | public class MissingFunctionException(string message) : Exception(message); -------------------------------------------------------------------------------- /src/Exceptions/MissingLibraryException.cs: -------------------------------------------------------------------------------- 1 | namespace LibLoader.Exceptions; 2 | 3 | public class MissingLibraryException(string message) : Exception(message); -------------------------------------------------------------------------------- /src/Exceptions/NativeFunctionCallException.cs: -------------------------------------------------------------------------------- 1 | namespace LibLoader.Exceptions; 2 | 3 | public class NativeFunctionCallException : Exception 4 | { 5 | public NativeFunctionCallException(string message, Exception innerException) : base(message, innerException) 6 | { 7 | 8 | } 9 | 10 | public NativeFunctionCallException(string message) : base(message) 11 | { 12 | } 13 | } -------------------------------------------------------------------------------- /src/Exceptions/UnsupportedArchitectureException.cs: -------------------------------------------------------------------------------- 1 | namespace LibLoader.Exceptions; 2 | 3 | public class UnsupportedArchitectureException(string message) : Exception(message); -------------------------------------------------------------------------------- /src/Exceptions/UnsupportedParameterTypeException.cs: -------------------------------------------------------------------------------- 1 | namespace LibLoader.Exceptions; 2 | 3 | public class UnsupportedParameterTypeException(string message) : Exception(message); -------------------------------------------------------------------------------- /src/Exceptions/UnsupportedPlatformException.cs: -------------------------------------------------------------------------------- 1 | namespace LibLoader.Exceptions; 2 | 3 | public class UnsupportedPlatformException(string message) : Exception(message); -------------------------------------------------------------------------------- /src/Interfaces/ILibraryDefinitionBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace LibLoader.Interfaces; 2 | 3 | /// 4 | /// Interface for building definitions for native libraries. 5 | /// 6 | public interface ILibraryDefinitionBuilder 7 | { 8 | /// 9 | /// Sets the name of the library. 10 | /// 11 | /// The name of the library. 12 | /// If true, automatically converts the name to the platform-specific format. 13 | /// The ILibraryDefinitionBuilder instance for chaining method calls. 14 | ILibraryDefinitionBuilder WithName(string libraryName, bool usePlatformSpecificName = true); 15 | 16 | /// 17 | /// Sets the version of the library. 18 | /// 19 | /// The version of the library (optional). 20 | /// The ILibraryDefinitionBuilder instance for chaining method calls. 21 | ILibraryDefinitionBuilder WithVersion(string? version); 22 | 23 | /// 24 | /// Sets the custom path to the library. 25 | /// 26 | /// The custom path to the library. 27 | /// The ILibraryDefinitionBuilder instance for chaining method calls. 28 | ILibraryDefinitionBuilder FromCustomPath(string? customPath); 29 | 30 | /// 31 | /// Sets the remote URL from which to download the library. 32 | /// 33 | /// The remote URL. 34 | /// The file name of the library in the remote location (optional). 35 | /// The ILibraryDefinitionBuilder instance for chaining method calls. 36 | ILibraryDefinitionBuilder FromRemoteUrl(string? remoteUrl, string? remoteFileName = null); 37 | 38 | /// 39 | /// Sets the dependencies of the library. 40 | /// 41 | /// An array of (name, version) tuples representing the dependencies. 42 | /// The ILibraryDefinitionBuilder instance for chaining method calls. 43 | ILibraryDefinitionBuilder WithDependencies(params (string name, string version)[] dependencies); 44 | 45 | /// 46 | /// Sets a condition function that determines whether to load this library. 47 | /// 48 | /// The condition function. 49 | /// The ILibraryDefinitionBuilder instance for chaining method calls. 50 | ILibraryDefinitionBuilder WithCondition(Func? condition); 51 | 52 | /// 53 | /// Starts building a native function definition for the library. 54 | /// 55 | /// A native function builder instance. 56 | INativeFunctionBuilder WithNativeFunction(); 57 | 58 | /// 59 | /// Adds the library definition to the LibLoader instance. 60 | /// 61 | /// The platform builder instance for chaining method calls. 62 | IPlatformBuilder Add(); 63 | } -------------------------------------------------------------------------------- /src/Interfaces/INativeFunctionBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace LibLoader.Interfaces; 4 | 5 | /// 6 | /// Interface for building definitions for native functions within a library. 7 | /// 8 | public interface INativeFunctionBuilder 9 | { 10 | /// 11 | /// Sets the native name of the function. 12 | /// 13 | /// The native name of the function. 14 | /// The INativeFunctionBuilder instance for chaining method calls. 15 | INativeFunctionBuilder Named(string nativeName); 16 | 17 | /// 18 | /// Sets the calling name of the function (used in managed code). 19 | /// 20 | /// The calling name of the function. 21 | /// The INativeFunctionBuilder instance for chaining method calls. 22 | INativeFunctionBuilder WithCallingName(string callingName); 23 | 24 | /// 25 | /// Sets the calling convention used for the function. 26 | /// 27 | /// The calling convention. 28 | /// The INativeFunctionBuilder instance for chaining method calls. 29 | INativeFunctionBuilder WithCallingConvention(CallingConvention callingConvention); 30 | 31 | /// 32 | /// Sets the return type of the function using a generic type parameter. 33 | /// 34 | /// The return type of the function. 35 | /// The INativeFunctionBuilder instance for chaining method calls. 36 | INativeFunctionBuilder WithReturnType(); 37 | 38 | /// 39 | /// Sets the return type of the function using a Type object. 40 | /// 41 | /// The return type of the function. 42 | /// The INativeFunctionBuilder instance for chaining method calls. 43 | INativeFunctionBuilder WithReturnType(Type returnType); 44 | 45 | /// 46 | /// Adds a parameter to the function using a generic type parameter. 47 | /// 48 | /// The type of the parameter. 49 | /// The name of the parameter. 50 | /// The INativeFunctionBuilder instance for chaining method calls. 51 | INativeFunctionBuilder WithParameter(string name); 52 | 53 | /// 54 | /// Adds the native function definition to the library. 55 | /// 56 | /// The library definition builder instance for chaining method calls. 57 | ILibraryDefinitionBuilder Add(); 58 | } -------------------------------------------------------------------------------- /src/Interfaces/IPlatformBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace LibLoader.Interfaces; 2 | 3 | /// 4 | /// Interface for building definitions for native libraries and dependencies for a specific platform. 5 | /// 6 | public interface IPlatformBuilder 7 | { 8 | /// 9 | /// Starts building a definition for a native library. 10 | /// 11 | /// A library definition builder instance. 12 | ILibraryDefinitionBuilder WithLibrary(); 13 | 14 | /// 15 | /// Starts building a definition for a dependency library. 16 | /// 17 | /// A dependency definition builder instance. 18 | ILibraryDefinitionBuilder WithDependency(); 19 | 20 | /// 21 | /// Builds the LibLoader instance with the defined libraries and dependencies. 22 | /// 23 | /// The LibLoader instance. 24 | LibLoader Build(); 25 | } -------------------------------------------------------------------------------- /src/LibLoader.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Compression; 2 | using System.Reflection; 3 | using System.Runtime.InteropServices; 4 | using LibLoader.Builders; 5 | using LibLoader.Exceptions; 6 | using LibLoader.Interfaces; 7 | using LibLoader.Models; 8 | using LibLoader.Native; 9 | 10 | namespace LibLoader; 11 | 12 | /// 13 | /// Defines the platform for which the native libraries are targeted. 14 | /// 15 | public enum Platform 16 | { 17 | /// 18 | /// Represents the Windows platform. 19 | /// 20 | Windows, 21 | 22 | /// 23 | /// Represents the Linux platform. 24 | /// 25 | Linux, 26 | 27 | /// 28 | /// Represents the macOS platform. 29 | /// 30 | MacOs, 31 | 32 | /// 33 | /// Represents the Android platform. 34 | /// 35 | Android, 36 | 37 | /// 38 | /// Represents the iOS platform. 39 | /// 40 | Ios 41 | } 42 | 43 | /// 44 | /// Defines the bitness (32-bit or 64-bit) of the native libraries. 45 | /// 46 | public enum Bitness 47 | { 48 | /// 49 | /// Represents 32-bit native libraries. 50 | /// 51 | X86, 52 | 53 | /// 54 | /// Represents 64-bit native libraries. 55 | /// 56 | X64, 57 | 58 | /// 59 | /// Represents ARM 32-bit native libraries. 60 | /// 61 | Arm, 62 | 63 | /// 64 | /// Represents ARM 64-bit native libraries. 65 | /// 66 | Arm64 67 | } 68 | 69 | /// 70 | /// Defines the log levels for library loading operations. 71 | /// 72 | public enum LogLevel 73 | { 74 | /// 75 | /// Informational log level. 76 | /// 77 | Info, 78 | 79 | /// 80 | /// Warning log level. 81 | /// 82 | Warning, 83 | 84 | /// 85 | /// Error log level. 86 | /// 87 | Error 88 | } 89 | 90 | /// 91 | /// Delegate for logging events during library loading. 92 | /// 93 | /// The log level of the event. 94 | /// The log message. 95 | /// An optional exception associated with the event. 96 | public delegate void LogDelegate(LogLevel level, string message, Exception? ex = null); 97 | 98 | /// 99 | /// Manages the loading and unloading of native libraries. 100 | /// 101 | public class LibLoader 102 | { 103 | public LogDelegate Log { get; set; } = (level, s, _) => 104 | Console.WriteLine($"[{level}] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {s}"); 105 | 106 | internal readonly List Libraries = []; 107 | internal readonly Dictionary, List> Dependencies = new(); 108 | private readonly Dictionary, List> _loadedLibraries = new(); 109 | private readonly Dictionary _downloadedFiles = new(); 110 | private readonly HashSet _loadedLibraryPaths = new(); 111 | private string _targetDirectory = Path.Combine(Path.GetTempPath(), "Native"); 112 | private bool _loadLibraryExplicit = true; 113 | private IProgress? _progress; 114 | private float _overallProgress; 115 | private int _totalLibrariesToLoad; 116 | private CancellationToken _cancellationToken; 117 | private readonly List _searchPaths = []; 118 | 119 | /// 120 | /// Gets the singleton instance of the LibLoader class. 121 | /// 122 | public static LibLoader Instance => _instance.Value; 123 | private static readonly Lazy _instance = new(() => new LibLoader()); 124 | 125 | /// 126 | /// Initializes a new instance of the LibLoader class. 127 | /// 128 | /// 129 | /// 130 | /// This constructor is private to ensure that the singleton instance is initialized only once and only accessible through the property. 131 | /// 132 | /// 133 | private LibLoader() { } 134 | 135 | 136 | /// 137 | /// Sets the target directory for storing downloaded or extracted native libraries. 138 | /// 139 | /// The target directory path. 140 | /// The LibLoader instance for chaining method calls. 141 | public LibLoader WithTargetDirectory(string targetDirectory) 142 | { 143 | _targetDirectory = targetDirectory; 144 | return this; 145 | } 146 | 147 | /// 148 | /// Sets whether libraries should be loaded implicitly or explicitly. 149 | /// 150 | /// True to load libraries implicitly; false to load explicitly. 151 | /// The LibLoader instance for chaining method calls. 152 | public LibLoader WithImplicitLoading(bool loadLibraryImplicit = true) 153 | { 154 | _loadLibraryExplicit = !loadLibraryImplicit; 155 | return this; 156 | } 157 | 158 | /// 159 | /// Sets a progress reporter for tracking library loading progress. 160 | /// 161 | /// The progress reporter. 162 | /// The LibLoader instance for chaining method calls. 163 | public LibLoader WithProgress(IProgress progress) 164 | { 165 | _progress = progress; 166 | return this; 167 | } 168 | 169 | /// 170 | /// Sets a cancellation token for library loading operations. 171 | /// 172 | /// The cancellation token. 173 | /// The LibLoader instance for chaining method calls. 174 | public LibLoader WithCancellationToken(CancellationToken cancellationToken) 175 | { 176 | _cancellationToken = cancellationToken; 177 | return this; 178 | } 179 | 180 | /// 181 | /// Adds search paths for locating native libraries. 182 | /// 183 | /// The search paths to add. 184 | /// The LibLoader instance for chaining method calls. 185 | public LibLoader WithSearchPaths(IEnumerable searchPaths) 186 | { 187 | _searchPaths.AddRange(searchPaths); 188 | return this; 189 | } 190 | 191 | /// 192 | /// Starts building a library definition for a specific platform and bitness. 193 | /// 194 | /// The target platform. 195 | /// The target bitness. 196 | /// A platform builder instance. 197 | public IPlatformBuilder ForPlatform(Platform platform, Bitness bitness) 198 | { 199 | return new PlatformBuilder(platform, bitness); 200 | } 201 | 202 | /// 203 | /// Asynchronously loads the defined native libraries. 204 | /// 205 | /// A task representing the loading operation. 206 | public async Task LoadAsync() 207 | { 208 | var platform = GetPlatform(); 209 | var bitness = GetBitness(); 210 | 211 | _totalLibrariesToLoad = Libraries.Count(lib => lib.Condition == null || lib.Condition()); 212 | _overallProgress = 0; 213 | _progress?.Report(0); 214 | 215 | foreach (var library in Libraries) 216 | { 217 | try 218 | { 219 | await LoadLibraryAsync(library, platform, bitness); 220 | _overallProgress += 1f / _totalLibrariesToLoad; 221 | _progress?.Report(_overallProgress); 222 | } 223 | catch (Exception ex) when (ex is not MissingLibraryException) 224 | { 225 | Log(LogLevel.Error, $"Failed to load library '{library.LibraryName}' asynchronously", ex); 226 | throw; // Re-throw if it's not a MissingLibraryException 227 | } 228 | } 229 | 230 | _progress?.Report(1f); 231 | } 232 | 233 | /// 234 | /// Unloads all loaded native libraries. 235 | /// 236 | public void Unload() 237 | { 238 | foreach (var libInfo in _loadedLibraries.Keys.SelectMany(platformBitness => 239 | _loadedLibraries[platformBitness].Where(libInfo => libInfo.Loaded))) 240 | { 241 | try 242 | { 243 | if (_loadLibraryExplicit && libInfo.LibraryHandle != nint.Zero) 244 | NativeLibrary.Free(libInfo.LibraryHandle); 245 | 246 | libInfo.Loaded = false; 247 | libInfo.LibraryHandle = nint.Zero; 248 | libInfo.LoadedLibraryPath = string.Empty; 249 | } 250 | catch (Exception ex) 251 | { 252 | Log(LogLevel.Error, $"Failed to unload library '{libInfo.LibraryName}'", ex); 253 | } 254 | } 255 | 256 | _loadedLibraries.Clear(); 257 | DelegateFactory.Clear(); 258 | } 259 | 260 | /// 261 | /// Loads a single native library asynchronously. 262 | /// 263 | /// The library definition. 264 | /// The target platform. 265 | /// The target bitness. 266 | /// A task representing the loading operation. 267 | private async Task LoadLibraryAsync(LibraryDefinition libraryInfo, Platform platform, Bitness bitness) 268 | { 269 | if (libraryInfo.Loaded) return; // Don't load if already loaded 270 | // Check the condition if it exists 271 | if (libraryInfo.Condition != null && !libraryInfo.Condition()) 272 | { 273 | Log(LogLevel.Info, $"Skipping library '{libraryInfo.LibraryName}' because the condition was not met."); 274 | return; // Skip loading if the condition is false 275 | } 276 | 277 | var libraryPath = await ResolveLibraryPathAsync(libraryInfo); 278 | if (string.IsNullOrEmpty(libraryPath)) 279 | { 280 | Log(LogLevel.Error, $"Native library '{libraryInfo.LibraryName}' not found."); 281 | throw new MissingLibraryException( 282 | $"Native library '{libraryInfo.LibraryName}' not found."); 283 | } 284 | 285 | // Dependency Loading 286 | if (libraryInfo.Dependencies != null) 287 | { 288 | foreach (var (depName, depVersion) in libraryInfo.Dependencies) 289 | { 290 | await LoadDependencyAsync(depName, depVersion, platform, bitness); 291 | } 292 | } 293 | 294 | // Duplicate Loading Prevention 295 | if (_loadedLibraryPaths.Contains(libraryPath)) 296 | { 297 | Log(LogLevel.Warning, 298 | $"Library '{libraryInfo.LibraryName}' already loaded from path '{libraryPath}'. Skipping."); 299 | libraryInfo.Loaded = true; 300 | return; 301 | } 302 | 303 | if (_loadLibraryExplicit) 304 | { 305 | libraryInfo.LibraryHandle = LoadNativeLibraryExplicitly(libraryPath); 306 | libraryInfo.LoadedLibraryPath = libraryPath; 307 | 308 | if (!_loadedLibraries.TryGetValue(Tuple.Create(platform, bitness), out var libraryInfos)) 309 | { 310 | libraryInfos = new List(); 311 | _loadedLibraries.Add(Tuple.Create(platform, bitness), libraryInfos); 312 | } 313 | 314 | libraryInfos.Add(libraryInfo); 315 | _loadedLibraryPaths.Add(libraryPath); 316 | } 317 | else 318 | { 319 | libraryInfo.LoadedLibraryPath = libraryPath; 320 | _loadedLibraryPaths.Add(libraryPath); 321 | } 322 | 323 | libraryInfo.Loaded = true; 324 | } 325 | 326 | /// 327 | /// Loads a single dependency asynchronously. 328 | /// 329 | /// The dependency name. 330 | /// The dependency version. 331 | /// The target platform. 332 | /// The target bitness. 333 | /// A task representing the loading operation. 334 | private async Task LoadDependencyAsync(string depName, string depVersion, Platform platform, Bitness bitness) 335 | { 336 | if (!Dependencies.TryGetValue(Tuple.Create(platform, bitness), out var dependencyList)) 337 | { 338 | Log(LogLevel.Error, $"No dependencies defined for platform '{platform}' and bitness '{bitness}'."); 339 | throw new MissingLibraryException( 340 | $"No dependencies defined for platform '{platform}' and bitness '{bitness}'."); 341 | } 342 | 343 | // Find matching dependency, considering version 344 | var dependency = dependencyList.FirstOrDefault(depInfo => 345 | depInfo.LibraryName == GetPlatformSpecificName(depName, platform) && 346 | (string.IsNullOrEmpty(depVersion) || depInfo.Version.ToString() == depVersion)); 347 | 348 | if (dependency == null) 349 | { 350 | throw new MissingLibraryException($"Dependency '{depName}' (version '{depVersion}') not found."); 351 | } 352 | 353 | // Handle remote dependencies: 354 | if (!string.IsNullOrEmpty(dependency.RemoteUrl)) 355 | { 356 | var dependencyPath = await DownloadAndExtractLibraryAsync(dependency.LibraryName, dependency.RemoteUrl, 357 | dependency.RemoteFileName); 358 | if (string.IsNullOrEmpty(dependencyPath)) 359 | { 360 | throw new MissingLibraryException($"Failed to download and extract dependency '{depName}'."); 361 | } 362 | 363 | dependency.CustomPath = dependencyPath; 364 | } 365 | 366 | // Use LoadLibrary to handle the dependency's dependencies recursively and loading 367 | await LoadLibraryAsync(dependency, platform, bitness); 368 | } 369 | 370 | /// 371 | /// Resolves the path to a native library. 372 | /// 373 | /// The library definition. 374 | /// A task that returns the path to the library, or an empty string if not found. 375 | private async Task ResolveLibraryPathAsync(LibraryDefinition libraryInfo) 376 | { 377 | if (!string.IsNullOrEmpty(libraryInfo.RemoteUrl)) 378 | { 379 | // Check if the library has already been downloaded 380 | return _downloadedFiles.TryGetValue(libraryInfo.RemoteUrl, out var value) 381 | ? value 382 | : // Return the local path if already downloaded 383 | await DownloadAndExtractLibraryAsync(libraryInfo.LibraryName, libraryInfo.RemoteUrl, 384 | libraryInfo.RemoteFileName); 385 | } 386 | 387 | if (!string.IsNullOrEmpty(libraryInfo.CustomPath)) 388 | { 389 | // Check if custom path refers to embedded resource 390 | if (libraryInfo.CustomPath.StartsWith("embedded:")) 391 | { 392 | var resourcePath = libraryInfo.CustomPath["embedded:".Length..]; 393 | return ExtractLibraryFromResources(libraryInfo.LibraryName, resourcePath); 394 | } 395 | 396 | // Treat custom path as a direct path or relative to the target directory 397 | var path = Path.IsPathRooted(libraryInfo.CustomPath) 398 | ? libraryInfo.CustomPath 399 | : Path.Combine(_targetDirectory, libraryInfo.CustomPath); 400 | 401 | if (File.Exists(path)) 402 | return path; 403 | 404 | Log(LogLevel.Warning, 405 | $"Custom path '{libraryInfo.CustomPath}' for '{libraryInfo.LibraryName}' does not exist."); 406 | } 407 | 408 | // Fallback: Search in standard search paths 409 | return FindLibraryInSearchPaths(libraryInfo.LibraryName); 410 | } 411 | 412 | /// 413 | /// Downloads and extracts a native library from a remote URL. 414 | /// 415 | /// The name of the library. 416 | /// The URL to download the library from. 417 | /// The file name of the remote library (optional). 418 | /// A task that returns the path to the downloaded library, or an empty string if the download or extraction fails. 419 | private async Task DownloadAndExtractLibraryAsync(string libraryName, string remoteUrl, 420 | string? remoteFileName) 421 | { 422 | try 423 | { 424 | var httpClient = new HttpClient(); 425 | var response = 426 | await httpClient.GetAsync(remoteUrl, HttpCompletionOption.ResponseHeadersRead, _cancellationToken); 427 | response.EnsureSuccessStatusCode(); 428 | 429 | var contentLength = response.Content.Headers.ContentLength ?? 0; 430 | var buffer = new byte[8192]; 431 | long receivedBytes = 0; 432 | 433 | var tempDir = Path.Combine(Path.GetTempPath(), "LibLoaderRemote"); 434 | Directory.CreateDirectory(tempDir); 435 | 436 | // Always preserve the original extension from the URL for downloaded file 437 | var originalExtension = Path.GetExtension(remoteUrl); 438 | var downloadFileName = remoteFileName ?? libraryName; 439 | 440 | // If remoteFileName doesn't match the URL extension, use URL extension for download 441 | var downloadPath = Path.Combine(tempDir, 442 | Path.GetExtension(downloadFileName).Equals(originalExtension, StringComparison.OrdinalIgnoreCase) 443 | ? downloadFileName 444 | : Path.ChangeExtension(downloadFileName, originalExtension)); 445 | 446 | // Save the downloaded file 447 | await using (var fileStream = new FileStream(downloadPath, FileMode.Create, FileAccess.Write)) 448 | await using (var contentStream = await response.Content.ReadAsStreamAsync(_cancellationToken)) 449 | { 450 | int bytesRead; 451 | while ((bytesRead = await contentStream.ReadAsync(buffer, _cancellationToken)) > 0) 452 | { 453 | await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), _cancellationToken); 454 | receivedBytes += bytesRead; 455 | var downloadProgress = (float)receivedBytes / contentLength; 456 | _progress?.Report(_overallProgress + (downloadProgress / _totalLibrariesToLoad)); 457 | } 458 | } 459 | 460 | _downloadedFiles[remoteUrl] = downloadPath; 461 | 462 | // Final path for the extracted/renamed library 463 | var finalLibraryPath = Path.Combine(tempDir, remoteFileName ?? libraryName); 464 | 465 | // Handle archives 466 | if (originalExtension.Equals(".zip", StringComparison.OrdinalIgnoreCase)) 467 | { 468 | await using var archiveStream = File.OpenRead(downloadPath); 469 | using var archive = new ZipArchive(archiveStream, ZipArchiveMode.Read); 470 | 471 | // Look for the library file in the archive 472 | var libraryFile = archive.Entries.FirstOrDefault(e => 473 | Path.GetFileName(e.FullName) 474 | .Equals(remoteFileName ?? libraryName, StringComparison.OrdinalIgnoreCase)); 475 | 476 | if (libraryFile == null) 477 | throw new MissingLibraryException( 478 | $"Library '{remoteFileName ?? libraryName}' not found in archive."); 479 | 480 | // Extract to the final path 481 | libraryFile.ExtractToFile(finalLibraryPath, true); 482 | return finalLibraryPath; 483 | } 484 | 485 | // If not an archive, just return the download path 486 | return downloadPath; 487 | } 488 | catch (Exception ex) 489 | { 490 | Log(LogLevel.Error, $"Failed to download library '{libraryName}' from '{remoteUrl}'", ex); 491 | return string.Empty; 492 | } 493 | } 494 | 495 | /// 496 | /// Gets the platform of the current operating system. 497 | /// 498 | /// The platform of the current operating system. 499 | private static Platform GetPlatform() 500 | { 501 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return Platform.Windows; 502 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return Platform.Linux; 503 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return Platform.MacOs; 504 | 505 | if (OperatingSystem.IsAndroid()) return Platform.Android; 506 | if (OperatingSystem.IsIOS()) return Platform.Ios; 507 | 508 | throw new UnsupportedPlatformException("Unsupported operating system."); 509 | } 510 | 511 | /// 512 | /// Gets the bitness of the current architecture. 513 | /// 514 | /// The bitness of the current architecture. 515 | private static Bitness GetBitness() 516 | { 517 | return RuntimeInformation.ProcessArchitecture switch 518 | { 519 | Architecture.X86 => Bitness.X86, 520 | Architecture.X64 => Bitness.X64, 521 | Architecture.Arm => Bitness.Arm, 522 | Architecture.Arm64 => Bitness.Arm64, 523 | _ => throw new UnsupportedArchitectureException( 524 | $"Unsupported architecture: {RuntimeInformation.ProcessArchitecture}") 525 | }; 526 | } 527 | 528 | /// 529 | /// Extracts a native library from embedded resources. 530 | /// 531 | /// The name of the library. 532 | /// The path to the embedded resource. 533 | /// The path to the extracted library. 534 | private string ExtractLibraryFromResources(string libraryName, string embeddedResourcePath) 535 | { 536 | var assembly = Assembly.GetEntryAssembly(); 537 | using var resourceStream = assembly?.GetManifestResourceStream(embeddedResourcePath); 538 | if (resourceStream == null) 539 | throw new MissingLibraryException( 540 | $"Native library '{libraryName}' not found in embedded resources at path '{embeddedResourcePath}'."); 541 | 542 | Directory.CreateDirectory(_targetDirectory); 543 | var libraryPath = Path.Combine(_targetDirectory, libraryName); 544 | 545 | using var fileStream = new FileStream(libraryPath, FileMode.Create, FileAccess.Write); 546 | resourceStream.CopyTo(fileStream); 547 | 548 | return libraryPath; 549 | } 550 | 551 | /// 552 | /// Loads a native library explicitly. 553 | /// 554 | /// The path to the library. 555 | /// The handle to the library. 556 | private nint LoadNativeLibraryExplicitly(string libraryPath) 557 | { 558 | if (!File.Exists(libraryPath)) 559 | throw new MissingLibraryException($"Native library '{libraryPath}' not found."); 560 | 561 | var handle = NativeLibrary.Load(libraryPath); 562 | if (handle == nint.Zero) 563 | throw new LibraryLoadException($"Failed to load library '{libraryPath}'."); 564 | 565 | return handle; // Return the handle 566 | } 567 | 568 | /// 569 | /// Searches for a native library in the defined search paths. 570 | /// 571 | /// The name of the library to search for. 572 | /// The path to the library if found, or an empty string if not found. 573 | private string FindLibraryInSearchPaths(string libraryName) 574 | { 575 | var allSearchPaths = new List(_searchPaths) { AppDomain.CurrentDomain.BaseDirectory }; 576 | 577 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 578 | { 579 | allSearchPaths.Add(Environment.GetEnvironmentVariable("PATH")!); 580 | } 581 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 582 | { 583 | allSearchPaths.Add(Environment.GetEnvironmentVariable("LD_LIBRARY_PATH")!); 584 | allSearchPaths.Add("/usr/local/lib"); 585 | allSearchPaths.Add("/usr/lib"); 586 | } 587 | 588 | foreach (var searchPath in allSearchPaths) 589 | { 590 | if (string.IsNullOrEmpty(searchPath)) continue; 591 | foreach (var path in searchPath.Split(Path.PathSeparator)) 592 | { 593 | var libraryPath = Path.Combine(path, libraryName); 594 | if (File.Exists(libraryPath)) 595 | return libraryPath; 596 | } 597 | } 598 | 599 | return string.Empty; 600 | } 601 | 602 | /// 603 | /// Gets the platform-specific name for a library. 604 | /// 605 | /// The base name of the library. 606 | /// The target platform. 607 | /// The platform-specific name of the library. 608 | public static string GetPlatformSpecificName(string libraryName, Platform platform) 609 | { 610 | string extension; 611 | var prefix = string.Empty; 612 | 613 | switch (platform) 614 | { 615 | case Platform.Windows: 616 | extension = ".dll"; 617 | break; 618 | case Platform.Linux: 619 | prefix = "lib"; 620 | extension = ".so"; 621 | break; 622 | case Platform.MacOs: 623 | prefix = "lib"; 624 | extension = ".dylib"; 625 | break; 626 | case Platform.Android: 627 | prefix = "lib"; 628 | extension = ".so"; 629 | break; 630 | case Platform.Ios: 631 | prefix = "lib"; 632 | extension = ".a"; // Or .dylib, depending on the library type 633 | break; 634 | default: throw new UnsupportedPlatformException($"Unsupported Platform {platform}"); 635 | } 636 | 637 | return $"{prefix}{libraryName}{extension}"; 638 | } 639 | 640 | 641 | /// 642 | /// Calls a native function with the specified name in the specified library. 643 | /// 644 | /// The return type of the native function. 645 | /// The name of the library containing the function. 646 | /// The name of the native function to call. 647 | /// The arguments to pass to the native function. 648 | /// The return value of the native function, cast to the specified type T. 649 | public T Call(string libraryName, string functionName, params object[] arguments) 650 | { 651 | return (T)Call(typeof(T), libraryName, functionName, arguments)!; 652 | } 653 | 654 | /// 655 | /// Calls a native function with the specified name in the specified library, with a void return type. 656 | /// 657 | /// The name of the library containing the function. 658 | /// The name of the native function to call. 659 | /// The arguments to pass to the native function. 660 | public void Call(string libraryName, string functionName, params object[] arguments) 661 | { 662 | Call(typeof(void), libraryName, functionName, arguments); 663 | } 664 | 665 | /// 666 | /// Calls a native function with the specified name in the specified library. 667 | /// 668 | /// The return type of the native function. 669 | /// The name of the library containing the function. 670 | /// The name of the native function to call. 671 | /// The arguments to pass to the native function. 672 | /// The return value of the native function, cast to the specified returnType. 673 | public object? Call(Type returnType, string libraryName, string functionName, params object[] arguments) 674 | { 675 | var libraryInfo = Libraries.FirstOrDefault(lib => lib.LibraryName.Contains(libraryName)); 676 | if (libraryInfo is not { Loaded: true }) 677 | { 678 | throw new MissingLibraryException($"Library '{libraryName}' is not loaded."); 679 | } 680 | 681 | if (!libraryInfo.NativeFunctions.TryGetValue(functionName, out var functionDefinition)) 682 | { 683 | throw new MissingFunctionException( 684 | $"Native library '{libraryName}' does not contain function '{functionName}'."); 685 | } 686 | 687 | // Invoke and handle return value 688 | var result = functionDefinition.Delegate.DynamicInvoke(arguments); 689 | 690 | if (returnType == typeof(void)) 691 | { 692 | return null; 693 | } 694 | 695 | // Handle potential conversion issues. If the result can't be cast 696 | try 697 | { 698 | return Convert.ChangeType(result, returnType); 699 | } 700 | catch (InvalidCastException ex) 701 | { 702 | throw new InvalidCastException( 703 | $"The native function '{functionName}' returned a value that could not be converted to '{returnType}'.", 704 | ex); 705 | } 706 | } 707 | 708 | /// 709 | /// Internally calls a native function, being called from delegates. 710 | /// 711 | /// The definition of the native function. 712 | /// The arguments to pass to the function. 713 | /// The result of the function call. 714 | private object? InternalCall(NativeFunctionDefinition functionDefinition, object[] arguments) 715 | { 716 | if (!_loadLibraryExplicit && functionDefinition.Library.LibraryHandle == nint.Zero) 717 | functionDefinition.Library.LibraryHandle = NativeLibrary.Load(functionDefinition.Library.LoadedLibraryPath); 718 | 719 | var functionAddress = NativeLibrary.GetExport(functionDefinition.Library.LibraryHandle, functionDefinition.NativeName); 720 | if (functionAddress == nint.Zero) 721 | { 722 | throw new MissingFunctionException($"Function '{functionDefinition.NativeName}' not found in library."); 723 | } 724 | 725 | // Get or create the appropriate delegate type 726 | var delegateType = DelegateFactory.GetOrCreateDelegateType(functionDefinition); 727 | var delegateInstance = Marshal.GetDelegateForFunctionPointer(functionAddress, delegateType); 728 | var marshaledArguments = ParameterMarshaler.MarshalParameters(functionDefinition.Parameters, arguments); 729 | 730 | try 731 | { 732 | return delegateInstance.DynamicInvoke(marshaledArguments); 733 | } 734 | catch (TargetInvocationException tie) 735 | { 736 | throw tie.InnerException ?? tie; 737 | } 738 | finally 739 | { 740 | ParameterMarshaler.FreeMarshaledStrings(marshaledArguments); 741 | } 742 | } 743 | } -------------------------------------------------------------------------------- /src/LibLoader.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 1.0.0 8 | 1.0.0 9 | LibLoader 10 | LibLoader is a cross-platform native library loader for .NET, simplifying the process of loading and interacting with native libraries (DLLs, SOs, dylibs) in your C# applications. It supports dependency management, flexible loading options (local files, embedded resources, remote URLs), and provides a clean, fluent API for configuration and function calling. 11 | Copyright (c) 2024 LSXPrime 12 | https://github.com/LSXPrime/LibLoader 13 | LICENSE.md 14 | https://github.com/LSXPrime/LibLoader 15 | Github 16 | libloader-icon.png 17 | README.md 18 | native, library, loader, dll, so, dylib, cross-platform, interop, p/invoke 19 | 1.0.0 20 | https://github.com/LSXPrime/LibLoader/releases 21 | LSXPrime 22 | 23 | 24 | 25 | 26 | True 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Models/LibraryDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace LibLoader.Models; 2 | 3 | /// 4 | /// Represents a definition for a native library. 5 | /// 6 | public class LibraryDefinition 7 | { 8 | /// 9 | /// Gets or sets the name of the library. 10 | /// 11 | public string LibraryName { get; set; } = string.Empty; 12 | 13 | /// 14 | /// Gets or sets the version of the library. 15 | /// 16 | public Version Version { get; set; } = new(0, 0, 0); 17 | 18 | /// 19 | /// Gets or sets a custom path to the library. 20 | /// 21 | public string? CustomPath { get; set; } 22 | 23 | /// 24 | /// Gets or sets the remote URL from which to download the library. 25 | /// 26 | public string? RemoteUrl { get; set; } 27 | 28 | /// 29 | /// Gets or sets the file name of the library in the remote location. 30 | /// 31 | public string? RemoteFileName { get; set; } 32 | 33 | /// 34 | /// Gets or sets an array of dependency library names and versions. 35 | /// 36 | public (string name, string version)[]? Dependencies { get; set; } 37 | 38 | /// 39 | /// Gets or sets a condition function that determines whether to load this library. 40 | /// 41 | public Func? Condition { get; set; } 42 | 43 | /// 44 | /// Gets or sets a value indicating whether the library has been loaded. 45 | /// 46 | public bool Loaded { get; set; } 47 | 48 | /// 49 | /// Gets or sets the path to the loaded library. 50 | /// 51 | public string LoadedLibraryPath { get; set; } = string.Empty; 52 | 53 | /// 54 | /// Gets or sets the handle to the loaded library. 55 | /// 56 | public nint LibraryHandle { get; set; } 57 | 58 | /// 59 | /// Gets or sets a dictionary of native functions defined in the library. 60 | /// 61 | public Dictionary NativeFunctions { get; set; } = new(); 62 | } -------------------------------------------------------------------------------- /src/Models/NativeFunctionDefinition.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace LibLoader.Models; 4 | 5 | /// 6 | /// Represents a definition for a native function within a library. 7 | /// 8 | public class NativeFunctionDefinition 9 | { 10 | /// 11 | /// Gets the library that contains the function. 12 | /// 13 | public LibraryDefinition Library { get; init; } 14 | 15 | /// 16 | /// Gets or sets the calling name of the function (used in managed code). 17 | /// 18 | public string CallingName { get; init; } = string.Empty; 19 | 20 | /// 21 | /// Gets or sets the native name of the function (as it appears in the native library). 22 | /// 23 | public string NativeName { get; init; } = string.Empty; 24 | 25 | /// 26 | /// Gets or sets the calling convention used for the function. 27 | /// 28 | public CallingConvention CallingConvention { get; init; } = CallingConvention.Cdecl; 29 | 30 | /// 31 | /// Gets or sets the return type of the function. 32 | /// 33 | public Type ReturnType { get; init; } = typeof(void); 34 | 35 | /// 36 | /// Gets or sets a list of parameters for the function. 37 | /// 38 | public List Parameters { get; init; } = []; 39 | 40 | /// 41 | /// Gets or sets the delegate instance that wraps the native function call. 42 | /// 43 | public Delegate Delegate { get; set; } 44 | } 45 | 46 | /// 47 | /// Represents a parameter for a native function. 48 | /// 49 | public class NativeFunctionParameter 50 | { 51 | /// 52 | /// Gets or sets the name of the parameter. 53 | /// 54 | public string Name { get; init; } = string.Empty; 55 | 56 | /// 57 | /// Gets or sets the type of the parameter. 58 | /// 59 | public Type Type { get; init; } 60 | } -------------------------------------------------------------------------------- /src/Native/DelegateFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Reflection; 3 | using System.Reflection.Emit; 4 | using System.Runtime.InteropServices; 5 | using LibLoader.Models; 6 | 7 | namespace LibLoader.Native; 8 | 9 | /// 10 | /// Provides methods for creating and managing dynamic delegate types for native function calls. 11 | /// 12 | public static class DelegateFactory 13 | { 14 | private static readonly Dictionary> DynamicDelegateTypes = new(); 15 | 16 | /// 17 | /// Gets or creates a dynamic delegate type matching the specified native function definition. 18 | /// 19 | /// The native function definition. 20 | /// The dynamic delegate type. 21 | public static Type GetOrCreateDelegateType(NativeFunctionDefinition functionDefinition) 22 | { 23 | // Create a unique key for this delegate type signature 24 | var key = 25 | $"{functionDefinition.ReturnType.FullName}_{string.Join("_", functionDefinition.Parameters.Select(p => p.Type.FullName))}"; 26 | 27 | if (DynamicDelegateTypes.TryGetValue(key, out var weakRef) && weakRef.TryGetTarget(out var existingType)) 28 | { 29 | return existingType; 30 | } 31 | 32 | // Create a new delegate type using TypeBuilder 33 | var assemblyName = new AssemblyName($"DynamicDelegateAssembly_{Guid.NewGuid()}"); 34 | var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); 35 | var moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicDelegateModule"); 36 | 37 | var typeBuilder = moduleBuilder.DefineType( 38 | $"DynamicDelegate_{Guid.NewGuid()}", 39 | TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, 40 | typeof(MulticastDelegate) 41 | ); 42 | 43 | // Add UnmanagedFunctionPointerAttribute 44 | var ctorBuilder = typeBuilder.DefineConstructor( 45 | MethodAttributes.RTSpecialName | MethodAttributes.HideBySig | MethodAttributes.Public, 46 | CallingConventions.Standard, 47 | [typeof(object), typeof(IntPtr)] 48 | ); 49 | 50 | ctorBuilder.SetImplementationFlags(MethodImplAttributes.Runtime | MethodImplAttributes.Managed); 51 | 52 | // Define the Invoke method 53 | var parameterTypes = functionDefinition.Parameters.Select(p => p.Type).ToArray(); 54 | var invokeBuilder = typeBuilder.DefineMethod( 55 | "Invoke", 56 | MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, 57 | functionDefinition.ReturnType, 58 | parameterTypes 59 | ); 60 | 61 | invokeBuilder.SetImplementationFlags(MethodImplAttributes.Runtime | MethodImplAttributes.Managed); 62 | 63 | // Add UnmanagedFunctionPointer attribute 64 | var attributeConstructor = 65 | typeof(UnmanagedFunctionPointerAttribute).GetConstructor([typeof(CallingConvention)]); 66 | var attributeBuilder = new CustomAttributeBuilder( 67 | attributeConstructor!, 68 | [functionDefinition.CallingConvention] 69 | ); 70 | typeBuilder.SetCustomAttribute(attributeBuilder); 71 | 72 | var delegateType = typeBuilder.CreateType(); 73 | DynamicDelegateTypes[key] = new WeakReference(delegateType); 74 | 75 | return delegateType; 76 | } 77 | 78 | /// 79 | /// Creates a delegate wrapper for the specified native function definition. 80 | /// 81 | /// The native function definition. 82 | /// The type of the delegate to create. 83 | /// A delegate instance that wraps the native function call. 84 | public static Delegate CreateWrapperDelegate(NativeFunctionDefinition functionDefinition, Type delegateType) 85 | { 86 | var parameters = functionDefinition.Parameters 87 | .Select((p, i) => Expression.Parameter(p.Type, $"param{i}")) 88 | .ToArray(); 89 | 90 | var argsArrayExpr = Expression.NewArrayInit( 91 | typeof(object), 92 | parameters.Select(p => Expression.Convert(p, typeof(object))) 93 | ); 94 | 95 | var internalCallMethod = typeof(LibLoader).GetMethod( 96 | "InternalCall", 97 | BindingFlags.NonPublic | BindingFlags.Instance)!; 98 | 99 | var callExpr = Expression.Call( 100 | Expression.Constant(LibLoader.Instance), 101 | internalCallMethod, 102 | Expression.Constant(functionDefinition), 103 | argsArrayExpr 104 | ); 105 | 106 | if (functionDefinition.ReturnType == typeof(void)) 107 | { 108 | var voidBody = Expression.Block(callExpr, Expression.Empty()); 109 | return Expression.Lambda(delegateType, voidBody, parameters).Compile(); 110 | } 111 | 112 | var convertedResult = Expression.Convert(callExpr, functionDefinition.ReturnType); 113 | return Expression.Lambda(delegateType, convertedResult, parameters).Compile(); 114 | } 115 | 116 | /// 117 | /// Clears the cache of dynamic delegate types. 118 | /// 119 | public static void Clear() => DynamicDelegateTypes.Clear(); 120 | } -------------------------------------------------------------------------------- /src/Native/ParameterMarshaler.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | using LibLoader.Models; 4 | 5 | namespace LibLoader.Native; 6 | 7 | /// 8 | /// Provides methods for marshalling parameters between managed and native code. 9 | /// 10 | public static class ParameterMarshaler 11 | { 12 | private static readonly Dictionary> MarshalConverters = new() 13 | { 14 | { typeof(string), obj => Marshal.StringToHGlobalAnsi((string)obj) }, 15 | { typeof(int), obj => (int)obj }, 16 | { typeof(long), obj => (nint)(long)obj }, 17 | { typeof(bool), obj => (bool)obj ? 1 : 0 } 18 | }; 19 | 20 | /// 21 | /// Frees any memory allocated for marshaled strings in the provided arguments. 22 | /// 23 | /// The arguments to free memory from. 24 | public static void FreeMarshaledStrings(object[] args) 25 | { 26 | foreach (var arg in args) 27 | { 28 | if (arg is nint ptr && ptr != nint.Zero) 29 | { 30 | Marshal.FreeHGlobal(ptr); 31 | } 32 | } 33 | } 34 | 35 | /// 36 | /// Marshals the provided arguments based on the defined native function parameters. 37 | /// 38 | /// The native function parameters. 39 | /// The arguments to marshal. 40 | /// An array of marshaled arguments ready for native function invocation. 41 | public static object[] MarshalParameters(List parameters, object[] arguments) 42 | { 43 | if (parameters.Count != arguments.Length) 44 | { 45 | throw new ArgumentException( 46 | $"Incorrect number of arguments. Expected {parameters.Count}, received {arguments.Length}."); 47 | } 48 | 49 | var marshaledArguments = new object[arguments.Length]; 50 | for (var i = 0; i < arguments.Length; i++) 51 | { 52 | var parameter = parameters[i]; 53 | var argument = arguments[i]; 54 | 55 | // For struct parameters passed by value, we need to copy the struct 56 | if (parameter.Type.IsValueType && !parameter.Type.IsPrimitive) 57 | { 58 | marshaledArguments[i] = argument; 59 | } 60 | else 61 | { 62 | marshaledArguments[i] = MarshalParameter(parameter, argument); 63 | } 64 | } 65 | 66 | return marshaledArguments; 67 | } 68 | 69 | /// 70 | /// Marshals a single parameter based on its type and the provided argument. 71 | /// 72 | /// The native function parameter definition. 73 | /// The argument to marshal. 74 | /// The marshaled argument. 75 | private static object MarshalParameter(NativeFunctionParameter parameter, object? argument) 76 | { 77 | if (argument == null) 78 | { 79 | return nint.Zero; 80 | } 81 | 82 | var parameterType = parameter.Type; 83 | 84 | // Handle strings 85 | if (parameterType == typeof(string)) 86 | { 87 | return Marshal.StringToHGlobalAnsi((string)argument); 88 | } 89 | 90 | switch (parameterType.IsValueType) 91 | { 92 | // Handle struct types differently - pass by value 93 | case true when !parameterType.IsPrimitive: 94 | return argument; 95 | // Handle value types 96 | case true when IsBlittableType(parameterType): 97 | return argument; 98 | case true when parameterType.IsEnum: 99 | return Convert.ChangeType(argument, Enum.GetUnderlyingType(parameterType)); 100 | } 101 | 102 | // Handle arrays 103 | if (parameterType.IsArray) 104 | { 105 | return MarshalArray(parameter, argument); 106 | } 107 | 108 | // Handle classes 109 | if (parameterType.IsClass) 110 | { 111 | return MarshalClass(parameter, argument); 112 | } 113 | 114 | // Default case 115 | return argument; 116 | } 117 | 118 | /// 119 | /// Marshals an array parameter based on its element type and the provided argument. 120 | /// 121 | /// The native function parameter definition. 122 | /// The argument to marshal. 123 | /// The marshaled argument. 124 | private static object MarshalArray(NativeFunctionParameter parameter, object argument) 125 | { 126 | var array = (Array)argument; 127 | var elementType = parameter.Type.GetElementType(); 128 | 129 | if (elementType == null) 130 | { 131 | throw new ArgumentException($"Cannot determine element type for parameter {parameter.Name}"); 132 | } 133 | 134 | // Handle array of primitives 135 | if (IsBlittableType(elementType)) 136 | { 137 | var size = Marshal.SizeOf(elementType) * array.Length; 138 | var ptr = Marshal.AllocHGlobal(size); 139 | 140 | var buffer = new byte[size]; 141 | 142 | // Get the raw data into a byte array: 143 | Buffer.BlockCopy(array, 0, buffer, 0, size); 144 | Marshal.Copy(buffer, 0, ptr, size); 145 | return ptr; 146 | } 147 | 148 | // Handle array of structs 149 | if (elementType is { IsValueType: true, IsPrimitive: false }) 150 | { 151 | var size = Marshal.SizeOf(elementType) * array.Length; 152 | var ptr = Marshal.AllocHGlobal(size); 153 | 154 | for (var i = 0; i < array.Length; i++) 155 | { 156 | var offset = i * Marshal.SizeOf(elementType); 157 | Marshal.StructureToPtr(array.GetValue(i)!, ptr + offset, false); 158 | } 159 | 160 | return ptr; 161 | } 162 | 163 | // Handle array of objects/classes 164 | var marshaledArray = new object[array.Length]; 165 | for (var i = 0; i < array.Length; i++) 166 | { 167 | var elementParam = new NativeFunctionParameter 168 | { 169 | Type = elementType, 170 | Name = $"{parameter.Name}[{i}]" 171 | }; 172 | marshaledArray[i] = MarshalParameter(elementParam, array.GetValue(i)); 173 | } 174 | 175 | return marshaledArray; 176 | } 177 | 178 | /// 179 | /// Marshals a class parameter based on its type and the provided argument. 180 | /// 181 | /// The native function parameter definition. 182 | /// The argument to marshal. 183 | /// The marshaled argument. 184 | private static object MarshalClass(NativeFunctionParameter parameter, object argument) 185 | { 186 | var type = parameter.Type; 187 | 188 | // Handle special known types 189 | if (MarshalConverters.TryGetValue(type, out var converter)) 190 | { 191 | return converter(argument); 192 | } 193 | 194 | // Marshal class as struct if it has StructLayout attribute 195 | var layoutAttribute = type.GetCustomAttribute(); 196 | if (layoutAttribute != null) 197 | { 198 | var size = Marshal.SizeOf(type); 199 | var ptr = Marshal.AllocHGlobal(size); 200 | Marshal.StructureToPtr(argument, ptr, false); 201 | return ptr; 202 | } 203 | 204 | // For other classes, marshal public properties recursively 205 | var properties = type.GetProperties() 206 | .Where(p => p is { CanRead: true, CanWrite: true }) 207 | .ToList(); 208 | 209 | var marshaledObject = Activator.CreateInstance(type); 210 | foreach (var prop in properties) 211 | { 212 | var propParam = new NativeFunctionParameter 213 | { 214 | Type = prop.PropertyType, 215 | Name = prop.Name 216 | }; 217 | var marshaledValue = MarshalParameter(propParam, prop.GetValue(argument)); 218 | prop.SetValue(marshaledObject, marshaledValue); 219 | } 220 | 221 | return marshaledObject!; 222 | } 223 | 224 | /// 225 | /// Determines if a type is blittable (can be directly copied between managed and native memory). 226 | /// 227 | /// The type to check. 228 | /// True if the type is blittable; otherwise, false. 229 | private static bool IsBlittableType(Type type) 230 | { 231 | return type.IsPrimitive || 232 | type == typeof(decimal) || 233 | (type is { IsValueType: true, IsEnum: false } && 234 | type.GetFields(BindingFlags.Instance | 235 | BindingFlags.Public | 236 | BindingFlags.NonPublic) 237 | .All(field => IsBlittableType(field.FieldType))); 238 | } 239 | } -------------------------------------------------------------------------------- /src/libloader-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSXPrime/LibLoader/ede509e32fd47287778b49824a529dc08180a565/src/libloader-icon.png --------------------------------------------------------------------------------