Skip to content

Commit

Permalink
Fix dynamic library loading on macOS
Browse files Browse the repository at this point in the history
  • Loading branch information
lyonsil committed Jun 12, 2024
1 parent 0342df4 commit c0eba81
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 14 deletions.
57 changes: 52 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,40 @@ Sample code:

## Building

To build the current version of icu-dotnet you'll need .net 6.0 installed.
To build the current version of icu-dotnet you'll need .net 8.0 installed.

icu-dotnet can be built from the command line as well as Visual Studio or JetBrains Rider.

### Windows and Linux
### Running Unit Tests

You can build and run the unit tests by running:

```bash
dotnet test source/icu.net.sln
```

or, if wanting to run tests on just one specific .net version (v8.0 in this example):

```bash
dotnet test source/icu.net.sln -p:TargetFramework=net8.0
```

### Linux and macOS

It is important for `icu.net.dll.config` to be bundled with your application when not
running on Windows. If it doesn't copy reliably to the output directory, you might find
adding something like the following to your `csproj` file will resolve the issue. Note
that the version number in the path must match the version number of icu.net that is
referenced in the project.

```xml
<ItemGroup>
<None Update="$(NuGetPackageRoot)\icu.net\2.9.0\contentFiles\any\any\icu.net.dll.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
```

### Docker

icu-dotnet depends on libc dynamic libraries at run time. If running within Docker, you may
Expand All @@ -78,6 +100,10 @@ icu-dotnet links with any installed version of ICU shared objects. It is
recommended to install the version provided by the distribution. As of 2016,
Ubuntu Trusty uses version ICU 52 and Ubuntu Xenial 55.

If the version provided by the Linux distribution doesn't match your needs,
[Microsoft's ICU package](https://www.nuget.org/packages/Microsoft.ICU.ICU4C.Runtime/)
includes builds for Linux.

### Windows

Rather than using the full version of ICU (which can be ~25 MB), a custom minimum
Expand All @@ -87,6 +113,9 @@ The full version of ICU is also available as
[Icu4c.Win.Full.Lib](https://www.nuget.org/packages/Icu4c.Win.Full.Lib/) and
[Icu4c.Win.Full.Bin](https://www.nuget.org/packages/Icu4c.Win.Full.Bin/).

Microsoft also makes the full version available as
[Microsoft.ICU.ICU4C.Runtime](https://www.nuget.org/packages/Microsoft.ICU.ICU4C.Runtime/).

#### What's in the minimum build

- Characters
Expand All @@ -96,17 +125,35 @@ The full version of ICU is also available as
- Rules-based Collator
- Unicode set to pattern conversions

### macOS

macOS doesn't come preinstalled with all the normal icu4c libraries. They must be
installed separately. One option is to use [MacPorts](https://www.macports.org/).
The [icu package on MacPorts](https://ports.macports.org/port/icu/) has the icu4c
libraries needed for icu.net to run properly.

If the icu4c libraries are not installed in a directory that is in the system path
or your application directory, you will need to set an environment variable for
the OS to find them. For example:

```bash
export DYLD_FALLBACK_LIBRARY_PATH="$HOME/lib:/usr/local/lib:/usr/lib:/opt/local/lib"
```

If you need to set environment variables like the above, consider adding them to
your `.zprofile` so you don't have to remember to do it manually.

## Troubleshooting

- make sure you added the nuget packages `icu.net` and `Icu4c.Win.Min`
(or `Icu4c.Win.Full`).
- make sure you added the nuget package `icu.net` and have native ICU libraries available.
- the binaries of the nuget packages need to be copied to your output directory.
For `icu.net` this happens by the assembly reference that the package
adds to your project. The binaries of `Icu4c.Win.Min` are only relevant on
Windows. They will get copied by the `Icu4c.Win.Min.targets` file included
in the nuget package.

The package installer should have added an import to the `*.csproj` file similar to the following:
On Windows, the package installer should have added an import to the `*.csproj` file similar
to the following:

```xml
<Import Project="..\..\packages\Icu4c.Win.Min.54.1.31\build\Icu4c.Win.Min.targets"
Expand Down
85 changes: 76 additions & 9 deletions source/icu.net/NativeMethods/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ static NativeMethods()

#region Dynamic method loading

#region Native methods for Unix
#region Native methods for Linux

private const int RTLD_NOW = 2;

Expand Down Expand Up @@ -330,9 +330,11 @@ private static IntPtr GetIcuLibHandle(string basename, int icuVersion)
IntPtr handle;
string libPath;
int lastError;
string loadMethod;

if (IsWindows)
{
loadMethod = nameof(LoadLibraryEx);
var libName = $"{basename}{icuVersion}.dll";
var isIcuPathSpecified = !string.IsNullOrEmpty(_IcuPath);
libPath = isIcuPathSpecified ? Path.Combine(_IcuPath, libName) : libName;
Expand All @@ -346,9 +348,32 @@ private static IntPtr GetIcuLibHandle(string basename, int icuVersion)

Trace.WriteLineIf(handle == IntPtr.Zero && lastError != 0, $"Unable to load [{libPath}]. Error: {new Win32Exception(lastError).Message}");
}
else if (IsMac)
{
loadMethod = "NativeLibrary.Load";
var libName = $"lib{basename}.{icuVersion}.dylib";
libPath = string.IsNullOrEmpty(_IcuPath) ? libName : Path.Combine(_IcuPath, libName);
#if NET6_0_OR_GREATER
try
{
handle = NativeLibrary.Load(libPath);
}
catch (DllNotFoundException)
{
handle = IntPtr.Zero;
}
lastError = Marshal.GetLastWin32Error();
Trace.WriteLineIf(handle == IntPtr.Zero && lastError != 0, $"Unable to load [{libPath}]. Error: {lastError}");
#else
handle = IntPtr.Zero;
lastError = 0;
Trace.WriteLine($"Unable to load [{libPath}] on macOS without .NET 6 or higher.");
#endif
}
else
{
var libName = IsMac ? $"lib{basename}.{icuVersion}.dylib" : $"lib{basename}.so.{icuVersion}";
loadMethod = nameof(dlopen);
var libName = $"lib{basename}.so.{icuVersion}";
libPath = string.IsNullOrEmpty(_IcuPath) ? libName : Path.Combine(_IcuPath, libName);

handle = dlopen(libPath, RTLD_NOW);
Expand All @@ -363,7 +388,7 @@ private static IntPtr GetIcuLibHandle(string basename, int icuVersion)
return handle;
}

Trace.TraceWarning("{0} of {1} failed with error {2}", IsWindows ? "LoadLibraryEx" : "dlopen", libPath, lastError);
Trace.TraceWarning($"{loadMethod} of {libPath} failed with error {lastError}");
icuVersion -= 1;
}
}
Expand Down Expand Up @@ -403,6 +428,15 @@ internal static void Cleanup()
if (_IcuI18NLibHandle != IntPtr.Zero)
FreeLibrary(_IcuI18NLibHandle);
}
else if (IsMac)
{
#if NET6_0_OR_GREATER
if (_IcuCommonLibHandle != IntPtr.Zero)
NativeLibrary.Free(_IcuCommonLibHandle);
if (_IcuI18NLibHandle != IntPtr.Zero)
NativeLibrary.Free(_IcuI18NLibHandle);
#endif
}
else
{
if (_IcuCommonLibHandle != IntPtr.Zero)
Expand Down Expand Up @@ -437,16 +471,49 @@ private static void ResetIcuVersionInfo()
private static T GetMethod<T>(IntPtr handle, string methodName, bool missingInMinimal = false) where T : class
{
var versionedMethodName = $"{methodName}_{IcuVersion}";
var methodPointer = IsWindows ?
GetProcAddress(handle, versionedMethodName) :
dlsym(handle, versionedMethodName);
IntPtr methodPointer;
if (IsWindows)
methodPointer = GetProcAddress(handle, versionedMethodName);
else if (IsMac)
{
#if NET6_0_OR_GREATER
try
{
NativeLibrary.TryGetExport(handle, versionedMethodName, out methodPointer);
}
catch (DllNotFoundException)
{
methodPointer = IntPtr.Zero;
}
#else
methodPointer = IntPtr.Zero;
#endif
}
else
methodPointer = dlsym(handle, versionedMethodName);

// Some systems (eg. Tizen) don't use methods with IcuVersion suffix
if (methodPointer == IntPtr.Zero)
{
methodPointer = IsWindows ?
GetProcAddress(handle, methodName) :
dlsym(handle, methodName);
if (IsWindows)
methodPointer = GetProcAddress(handle, methodName);
else if (IsMac)
{
#if NET6_0_OR_GREATER
try
{
NativeLibrary.TryGetExport(handle, methodName, out methodPointer);
}
catch (DllNotFoundException)
{
methodPointer = IntPtr.Zero;
}
#else
methodPointer = IntPtr.Zero;
#endif
}
else
methodPointer = dlsym(handle, methodName);
}
if (methodPointer != IntPtr.Zero)
{
Expand Down

0 comments on commit c0eba81

Please sign in to comment.