FreeAndNil commented on issue #233:
URL:
https://github.com/apache/logging-log4net/issues/233#issuecomment-5197198339
### Reproduction
Reproduced on .NET 10, `PublishAot=true`, `linux-x64`, against `log4net`
built from `master`:
| API | JIT | Native AOT |
|---|---|---|
| `Assembly.GetCallingAssembly()` | ok | **throws
`PlatformNotSupportedException`** |
| `Assembly.GetExecutingAssembly()` | ok | ok |
| `Assembly.GetEntryAssembly()` | ok | ok |
| `typeof(X).Assembly` | ok | ok |
| `new StackTrace().GetFrame(1).GetMethod()` | ok | **returns `null`** |
So there is no stack-walking fallback available under AOT; the calling
assembly is simply not
recoverable at runtime.
`Assembly.GetCallingAssembly()` is called from **18 call sites** in
`src/log4net`, not just
`GetLogger`:
- `LogManager` — 9 (`Exists`, `GetCurrentLoggers`, `GetLogger(string)`,
`GetLogger(Type)`,
`ShutdownRepository`, `ResetConfiguration`, `GetRepository`,
`CreateRepository(Type)`, `Flush`)
- `XmlConfigurator` — 6
- `BasicConfigurator` — 2
- `SystemInfo.GetTypeFromString(string, bool, bool)` — 1
### Why new target frameworks alone will not fix it
Adding `net6.0`/`net8.0` TFMs plus `#if` does not solve this. AOT-ness is a
**publish mode**, not a
target framework - the same `net8.0` assembly runs both JIT-compiled and
AOT-compiled, and there is
no compile-time conditional that distinguishes the two. A newer TFM buys
exactly one thing:
`RuntimeFeature.IsDynamicCodeSupported` (netstandard2.1+/net5+) as a tidier
probe than a
`try`/`catch`. It is not required for a fix.
### Proposed fix, layer 1 - runtime probe (no API change, no new TFM)
A once-computed flag, with the `GetCallingAssembly()` call left **inline in
each public method**.
It cannot be extracted into a helper: the calling assembly of such a helper
is log4net itself, not
the assembly that called log4net.
```csharp
internal static class CallerAssembly
{
internal static bool IsSupported { get; } = Probe();
internal static Assembly Fallback { get; }
= Assembly.GetEntryAssembly() ?? typeof(CallerAssembly).Assembly;
private static bool Probe()
{
try
{
return Assembly.GetCallingAssembly() is not null;
}
catch (PlatformNotSupportedException)
{
return false;
}
}
}
```
Call sites become:
```csharp
public static ILog GetLogger(string name)
=> GetLogger(CallerAssembly.IsSupported ? Assembly.GetCallingAssembly() :
CallerAssembly.Fallback, name);
```
I validated the pattern with a two-assembly prototype published AOT: under
JIT it returns the true
calling assembly (the app, not the library); under AOT it returns the
fallback with no exception.
Cost: 18 mechanical edits, one static `bool`, no per-call overhead, works
as-is on
`net462`/`netstandard2.0`. Semantics degrade from "the caller's repository"
to "the entry
assembly's repository" - and since an AOT-published application is
self-contained, that is almost
always the same repository.
### Proposed fix, layer 2 - interceptor (accurate, opt-in, on top of layer 1)
The needed public overloads already exist: `LogManager.GetLogger(Assembly,
string)` and
`GetLogger(Assembly, Type)`. A source generator can rewrite
`LogManager.GetLogger(typeof(T))` at
the call site to pass `typeof(ContainingTypeOfTheCallSite).Assembly`, which
is *exactly* the calling
assembly — strictly more accurate than layer 1's fallback, and it removes
the reflection call
entirely. Use Roslyn's `SemanticModel.GetInterceptableLocation`; do not
hand-encode the v1
location blob.
Costs, which are why this should not be the only fix:
- a new generator project shipped as `analyzers/dotnet/cs` in the package,
plus a
`build/log4net.props` that appends log4net's namespace to
`<InterceptorsNamespaces>` so consumers
do not have to set it themselves
- interceptors are stable from the .NET 9.0.2xx SDK, so the *consumer* needs
a recent SDK
(log4net's own floor stays `net462`/`netstandard2.0`)
- only calls in compilations that run the generator are intercepted - a
pre-built third-party
library calling `LogManager.GetLogger(name)` still needs layer 1
- C# only; no help for VB/F# consumers
### Separate AOT blocker found while reproducing
Fixing `GetCallingAssembly` does **not** make log4net AOT-clean.
`SystemInfo`'s static constructor
reads `ConfigurationManager.AppSettings`, which fails under AOT:
```
System.Configuration.ConfigurationErrorsException: Configuration system
failed to initialize
---> System.MissingMethodException: No parameterless constructor defined for
type
'System.Configuration.ClientConfigurationHost'.
at System.Configuration.ConfigurationManager.get_AppSettings()
at log4net.Util.SystemInfo.GetAppSetting(String)
at log4net.Util.SystemInfo..cctor()
```
If `System.Configuration.ConfigurationManager` is not in the closure at all,
this surfaces as a
fatal `TypeInitializationException` (`Could not find file
'System.Configuration.ConfigurationManager'`) raised from
`LoggerManager.OnProcessExit`, which
aborts the process at shutdown. With the package present it degrades to a
logged `log4net:ERROR`.
Beyond that, `XmlConfigurator` instantiates appenders and layouts from type
names via
`Activator.CreateInstance`, which no amount of trim annotation can make
safe. Real AOT
configuration support means a code-first or source-generated configuration
path - worth tracking
separately so "fixes #233" is not read as "log4net is AOT-safe".
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]