Deployment

The user-mode library comes in two pieces, both of which must be deployed along with the application:

  1. A .NET assembly (managed), named callback.CBFSSync.dll (or callback.CBFSSync.NetStd.dll, for .NET Standard).
  2. A native dynamic library (unmanaged), named cbfssync26.dll, available for both 32-bit (x86) and 64-bit (x64, ARM64) processor architectures.

When deploying the application, copy both the .NET assembly and the native library to the target system and place them next to the application's executable file (on Windows, it has the .exe extension).

Alternatively, the native library may be placed into one of directories, the paths to which are contained in the

  • Windows: PATH environment variable, such as C:\Windows\System32 (or C:\Windows\SysWOW64 when deploying a 32-bit application on a 64-bit Windows system)
  • Linux: LD_LIBRARY_PATH environment variable
  • macOS: DYLD_LIBRARY_PATH environment variable

Windows:

The .NET assembly may be deployed to the Global Assembly Cache (GAC).

Loading of Native Library

Usually, .NET finds the native library when it is placed next to the assembly. But on some systems, it skips the directory of the assembly or even of the main application module. To ensure that this doesn't happen on a user's system when an application keeps the native library next to the .NET assembly, in the .NET Core 3.1 and .NET 5+ applications, you can use the DllImportResolver function as shown in the following snippet: public static IntPtr CBSyncImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { bool loaded = false; IntPtr libHandle = IntPtr.Zero; if (!libraryName.Contains("callback.CBFSSync")) return IntPtr.Zero; //Look for the native library next to the .NET assembly loaded = NativeLibrary.TryLoad(libraryName, assembly, DllImportSearchPath.AssemblyDirectory, out libHandle); //Look for the native library in the application's main directory if (!loaded) loaded = NativeLibrary.TryLoad(libraryName, assembly, DllImportSearchPath.ApplicationDirectory, out libHandle); //Look for the native library using teh default .NET behavior if (!loaded) loaded = NativeLibrary.TryLoad(libraryName, assembly, searchPath, out libHandle); return loaded ? libHandle : IntPtr.Zero; } ... NativeLibrary.SetDllImportResolver(Assembly.GetAssembly(typeof(CBSync)), CBSyncImportResolver); This code attempts to load the native library in the directory of the managed .NET assembly, and if not found then attempts to find the library in the application's main directory, and if not found then revert to the default .NET behavior. Only one DllImportResolver function may be set for the assembly, so doing the above for one component is sufficient.

In the .NET Framework 4.x applications, one can use AppDomain.CurrentDomain.AssemblyResolve event for the same purpose.

NuGet Notes

After the NuGet package is added to a project, both the managed .NET assembly and the unmanaged native library will be copied to the project's output directory anytime the project is built. However, the exact files copied to the output directory for the native library will vary based on the project type:

  • For .NET Core projects, a runtimes directory will be created in the output directory (if it does not already exist), and versions of the native library for each supported runtime identifier (RID) (e.g., win-x64) will be placed in the appropriate subdirectories. When the .NET Core application is distributed, the entire runtimes directory should be deployed alongside it.
  • For other types of projects (.NET Framework, UWP), only the native library version specific to the currently selected platform target (e.g., x64) will be copied to the output directory.
    • For .NET Framework projects specifically, please note that the project's platform target (Project > Properties > Build Tab > Platform target) must be set to a real architecture. If it is set to "Any CPU", no native library will be copied to the output directory.

Windows:

The native library may alternatively be installed to the Windows system directory. This approach allows deploying both the 32-bit and 64-bit versions of the native library simultaneously, because each gets placed into the system directory that corresponds to the appropriate processor architecture.

Threading and Concurrency

The component utilizes different underlying technologies on different operating systems. Because of that, the threading model exposed to applications depends in part on the platform component runs on.

NOTE: Even when configured for minimal concurrency, the component always fires events in the context of some worker thread, and not in the thread the component was originally created on. Therefore, applications must be sure to synchronize operations between event handlers and other threads as necessary (including, but not limited to, calls to the component instance, unless a method is explicitly documented as callable within events).

Configuring Event Concurrency

The SerializeEvents property controls whether events relating to different files should be allowed to fire in parallel on several worker threads, or serialized on a single worker thread. By default, this property is set to seOnMultipleThreads, and events for different files are allowed to fire in parallel.

Windows: Generally speaking, the component always enforces per-file event serialization; that is, events relating to the same file are always fired in sequence regardless of the property value. With per-file event serialization already ensured, the most important concurrency-related consideration is whether to enforce multifile event serialization as well, which is what SerializeEvents controls. When SerializeEvents is set to seOnMultipleThreads, the MinWorkerThreadCount, MaxWorkerThreadCount, and WorkerInitialStackSize configuration settings let the application tune the worker thread pool. These settings are ignored when SerializeEvents is set to seOnOneWorkerThread.

Linux: When SerializeEvents is set to seOnMultipleThreads, the component may fire events related to the same file concurrently in several threads. If the application is not prepared for that, it should use seOnOneWorkerThread mode.

macOS: The events always fire on a single worker thread, and the SerializeEvents property has no effect.