diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a95422795811..a7d82e382ade 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -44,6 +44,12 @@ The build system supports multiple platforms simultaneously: The `configure` script is used to select which platforms to build. This script must be run before `make`. +## MSBuild code + +Non-standard patterns in .targets files: + +- Always use `$(DeviceSpecificIntermediateOutputPath)` instead of `$(IntermediateOutputPath)`. + ## Binding System ### bgen (Binding Generator) @@ -331,4 +337,4 @@ try { } catch (Exception e) { // Code here } -``` \ No newline at end of file +``` diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index 899777386f62..f9e8d7ccb9fa 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -2,6 +2,7 @@ name: Linux Build Verification on: pull_request: + workflow_dispatch: permissions: contents: read diff --git a/Make.config b/Make.config index 2974ed71547e..93b90db4453b 100644 --- a/Make.config +++ b/Make.config @@ -397,6 +397,15 @@ DOTNET_DIR=$(abspath $(TOP)/builds/downloads/$(DOTNET_INSTALL_NAME)) export DOTNET_ROOT=$(DOTNET_DIR) # dotnet now is being looked up in the PATH export PATH := $(DOTNET_DIR):$(PATH) + +# Disable build servers to prevent parallel make from hanging. +# Build servers (MSBuild server, Roslyn/VBCSCompiler) inherit jobserver file +# descriptors from make, and don't close them when daemonizing. This prevents +# make from detecting that all jobs have finished, causing it to hang +# indefinitely at the end of the build. +export DOTNET_CLI_USE_MSBUILD_SERVER=0 +export UseSharedCompilation=false +export MSBUILDDISABLENODEREUSE=1 DOTNET=$(DOTNET_DIR)/dotnet DOTNET_BCL_DIR:=$(abspath $(TOP)/packages/microsoft.netcore.app.ref/$(DOTNET_BCL_VERSION)/ref/$(DOTNET_TFM)) # when bumping to a new .NET version, there may be a period when some parts of .NET is still on the old .NET version, so handle that here for DOTNET_BCL_DIR diff --git a/Makefile b/Makefile index 68482dab47b6..0dcd1bcd2fa1 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,8 @@ world: check-system @$(MAKE) reset-versions @$(MAKE) all -j8 @$(MAKE) install -j8 + @echo "Build is done, the following workloads were built:" + @$(DOTNET) workload list .PHONY: check-system check-system: @@ -68,9 +70,6 @@ install-hook:: exit 1; \ fi -all-hook install-hook:: - $(Q) $(MAKE) -C dotnet shutdown-build-server - dotnet-install-system: $(Q) $(MAKE) -C dotnet install-system diff --git a/docs/building-apps/build-properties.md b/docs/building-apps/build-properties.md index 98623c7160e6..fa4d54b8e52e 100644 --- a/docs/building-apps/build-properties.md +++ b/docs/building-apps/build-properties.md @@ -582,6 +582,28 @@ See also: * The [AlternateAppIcon](build-items.md#alternateappicon) item group. * The [AppIcon](#appicon) property. +## InlineDlfcnMethods + +Controls whether the build system replaces runtime calls to `ObjCRuntime.Dlfcn` methods with direct native symbol lookups at build time, eliminating the overhead of `dlsym` at runtime. + +The valid options are: + +* `compatibility`: Inlines dlfcn method calls but only creates native references for symbols used in `[Field]` attributes. This is more conservative and avoids link errors for symbols that don't exist at build time. +* `strict`: Inlines dlfcn method calls and creates native references for all symbols. This is more aggressive and may cause link errors if referenced native symbols don't exist. +* (empty): Disables inlining of dlfcn method calls. + +Default value: +* .NET 11+: `strict` when using NativeAOT (`PublishAot=true`), `compatibility` otherwise. +* .NET 10 and earlier: not set (disabled). + +Example: + +```xml + + compatibility + +``` + ## iOSMinimumVersion Specifies the minimum iOS version the app can run on. diff --git a/docs/code/native-symbols.md b/docs/code/native-symbols.md new file mode 100644 index 000000000000..8ff837ca2a41 --- /dev/null +++ b/docs/code/native-symbols.md @@ -0,0 +1,68 @@ +# Native symbols + +Native symbols can be referenced from managed code in several ways: + +* P/Invokes (DllImports) +* Calls to `dlsym`, which can happen through: + * The various APIs in `ObjCRuntime.Dlfcn` + * The various APIs in `System.Runtime.InteropServices.NativeLibrary` + * A P/Invoke directly into `dlsym` + +It's highly desirable to use a direct native reference to native symbols when building a mobile app, for a few reasons: + +* It's faster at runtime, and the app is smaller. +* If the referenced native symbol comes from a third-party static library, the + native linker can remove it if it's configured to remove unused code + (because the native linker can't see that the native symbol is in fact used + at runtime) unless there's a direct native reference to the symbol. + +On the other hand there's one scenario when a direct native reference is not desirable: when the native symbol does not exist. + +In order to create a direct native reference to native symbols, we need to know the names of those native symbols. + +## The `InlineDlfcnMethods` property + +This behavior is controlled by the `InlineDlfcnMethods` MSBuild property, which +has two modes: + +* `strict`: all calls to `ObjCRuntime.Dlfcn` APIs are inlined. +* `compatibility`: only calls that reference symbols from `[Field]` attributes are inlined. + +See the [build properties documentation](../building-apps/build-properties.md) for default values. + +## How it works + +During the build we try to collect the following: + +* Any property or field with the `[Foundation.Field]` attribute: we collect the symbol name. +* Any calls to the `ObjCRuntime.Dlfcn` APIs: we try to collect the symbol name (this might not always succeed, if the symbol name is not a constant). +* We don't process calls to `System.Runtime.InteropServices.NativeLibrary` at the moment (this may change in the future, if there's need). + +This is further complicated by the fact that we only want to create native +references for symbols that survive trimming. + +So we do the following: + +1. During trimming, two custom linker steps execute: + + * `InlineDlfcnMethodsStep`: for every symbol we've collected, this step + creates a P/Invoke to a native method that will return the address for + that symbol (using a direct native reference), and modifies the code + that fetches that symbol to call said P/Invoke. + * `GenerateInlinedDlfcnNativeCodeStep`: writes the complete list of + inlined symbols to a file (`inlined-dlfcn-symbols.txt`) for later + MSBuild targets to consume. + +2. After trimming, we figure out which of those symbols survived: + + * For ILTrim: the `_CollectPostILTrimInformation` MSBuild target inspects + the trimmed assemblies and collects all the inlined dlfcn P/Invokes that + survived. Per-assembly results are cached to speed up incremental builds. + * For NativeAOT: the `_CollectPostNativeAOTTrimInformation` MSBuild target + inspects the native object file (or static library) produced by NativeAOT, + collects all unresolved native references, and filters them against the + inlined dlfcn symbols to determine which survived. + +3. The `_PostTrimmingProcessing` MSBuild target takes the surviving symbols + from either path, generates the corresponding native C code, and adds it to + the list of files to compile and link into the final executable. diff --git a/dotnet/Makefile b/dotnet/Makefile index a807fe5d4ccf..bd150b2f5e6c 100644 --- a/dotnet/Makefile +++ b/dotnet/Makefile @@ -537,23 +537,3 @@ clean-local:: $(Q) $(DOTNET) restore package/workaround-for-maccore-issue-2427/restore.csproj /bl:package/workaround-for-maccore-issue-2427/restore.binlog $(MSBUILD_VERBOSITY) $(Q) touch $@ -# We need to shut down the builder server, because: -# We're using parallel make, and parallel make will start a jobserver, managed by file descriptors, where these file descriptors must be closed in all subprocesses for make to realize it's done. -# 'dotnet pack' might have started a build server -# The build server does not close any file descriptors it may have inherited when daemonizing itself. -# Thus the build server (which will still be alive after we're done building here) might have a file descriptor open which make is waiting for. -# The proper fix is to fix the build server to close its file descriptors. -# The intermediate working is to shut down the build server instead. An alternative solution would be to pass /p:UseSharedCompilation=false to 'dotnet pack' to disable the usage of the build server. -# -# The 'shutdown-build-server' is executed in a sub-make (and not as a dependency to the all-hook target), -# to make sure it's executed after everything else is built in this file. -all-hook:: - $(Q) $(MAKE) shutdown-build-server - -shutdown-build-server: - $(Q) echo "Shutting down build servers:" - $(Q) $(DOTNET) build-server shutdown | sed 's/^/ /' || true - $(Q) echo "Listing .NET processes still alive:" - $(Q) pgrep -lf "^$(DOTNET)" | sed 's/^/ /' || true - $(Q) echo "Killing the above mentioned processes." - $(Q) pkill -9 -f "^$(DOTNET)" | sed 's/^/ /' || true diff --git a/dotnet/targets/Microsoft.Sdk.Desktop.targets b/dotnet/targets/Microsoft.Sdk.Desktop.targets index 6b878200c30a..276822e3c2e4 100644 --- a/dotnet/targets/Microsoft.Sdk.Desktop.targets +++ b/dotnet/targets/Microsoft.Sdk.Desktop.targets @@ -3,8 +3,38 @@ + + + <_DotNetWatchVariable Include="@(RuntimeEnvironmentVariable)" Condition="'%(Identity)' == 'DOTNET_WATCH' And '%(Value)' == '1'" /> + + + <_IsDotNetWatch>true + true + true + + + + + + <_TtyPath>@(_TtyOutput) + $(_TtyPath) + $(_TtyPath) + + <_OpenArguments Condition="'$(XamarinDebugMode)' != ''">$(_OpenArguments) --env __XAMARIN_DEBUG_MODE__=$(XamarinDebugMode) <_OpenArguments Condition="'$(XamarinDebugPort)' != ''">$(_OpenArguments) --env __XAMARIN_DEBUG_PORT__=$(XamarinDebugPort) diff --git a/dotnet/targets/Microsoft.Sdk.Mobile.targets b/dotnet/targets/Microsoft.Sdk.Mobile.targets index 7ecf8141a073..db28965e02bd 100644 --- a/dotnet/targets/Microsoft.Sdk.Mobile.targets +++ b/dotnet/targets/Microsoft.Sdk.Mobile.targets @@ -138,7 +138,7 @@ diff --git a/dotnet/targets/Xamarin.Shared.Sdk.targets b/dotnet/targets/Xamarin.Shared.Sdk.targets index b4b62f461734..8c0de02f7174 100644 --- a/dotnet/targets/Xamarin.Shared.Sdk.targets +++ b/dotnet/targets/Xamarin.Shared.Sdk.targets @@ -8,6 +8,9 @@ + + + @@ -15,6 +18,13 @@ + + + + + strict + compatibility + + @@ -475,7 +487,16 @@ - + + + + + <_MonoLibrary Remove="@(_MonoRuntimeComponentDontLink -> '$(_MonoRuntimePackPathNative)%(Identity)')" /> + + + <_MonoLibrary Include="@(_MonoRuntimeComponentLink -> '$(_MonoRuntimePackPathNative)%(Identity)')" /> + + <_ComputeLinkerArgumentsDependsOn> @@ -626,7 +647,9 @@ @(_BundlerEnvironmentVariables -> 'EnvironmentVariable=Overwrite=%(Overwrite)|%(Identity)=%(Value)') @(_XamarinFrameworkAssemblies -> 'FrameworkAssembly=%(Filename)') Interpreter=$(MtouchInterpreter) + InlineDlfcnMethods=$(InlineDlfcnMethods) IntermediateLinkDir=$(IntermediateLinkDir) + IntermediateOutputPath=$(DeviceSpecificIntermediateOutputPath) InvariantGlobalization=$(InvariantGlobalization) HybridGlobalization=$(HybridGlobalization) ItemsDirectory=$(_LinkerItemsDirectory) @@ -780,6 +803,7 @@ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.Steps.MarkDispatcher" /> <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsHandler" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(InlineDlfcnMethods)' != ''" Type="Xamarin.Linker.Steps.InlineDlfcnMethodsStep" /> <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Type="Xamarin.Linker.DoneStep" /> @@ -1613,6 +1638,81 @@ + + + <_TrimmedAssembly Include="$(IntermediateLinkDir)*.dll" /> + + + + + + <_ILTrimSurvivingNativeSymbolsFile>$(DeviceSpecificIntermediateOutputPath)inlined-dlfcn\iltrim-surviving-native-symbols.txt + <_NativeAOTUnresolvedSymbolsFile>$(DeviceSpecificIntermediateOutputPath)nativeaot-unresolved-symbols.txt + <_NativeAOTSurvivingNativeSymbolsFile>$(DeviceSpecificIntermediateOutputPath)nativeaot-surviving-native-symbols.txt + + + + + + + + + + + + <_SurvivingNativeSymbolsFile Include="$(_ILTrimSurvivingNativeSymbolsFile)" Condition="Exists('$(_ILTrimSurvivingNativeSymbolsFile)')" /> + <_SurvivingNativeSymbolsFile Include="$(_NativeAOTSurvivingNativeSymbolsFile)" Condition="Exists('$(_NativeAOTSurvivingNativeSymbolsFile)')" /> + + + + + + + + <_PostTrimmingSourceFiles> + $(DeviceSpecificIntermediateOutputPath)posttrim-info-compiled/%(Filename).o + + + + + + + + <_CompiledPostTrimmingFiles Include="@(_PostTrimmingSourceFiles -> '%(OutputFile)')" /> + <_NativeExecutableObjectFiles Include="@(_CompiledPostTrimmingFiles)" /> + + + + <_CompileNativeExecutableDependsOn> $(_CompileNativeExecutableDependsOn); @@ -1669,6 +1769,7 @@ _ReadAppManifest; _WriteAppManifest; _CompileNativeExecutable; + _PostTrimmingProcessing; _ReidentifyDynamicLibraries; _AddSwiftLinkerFlags; _ComputeLinkerProperties; @@ -1677,6 +1778,26 @@ + + + + + + @@ -2585,6 +2706,28 @@ global using nfloat = global::System.Runtime.InteropServices.NFloat%3B + + + + <_HotReloadVariable Include="@(RuntimeEnvironmentVariable)" Condition="'%(Identity)' == 'DOTNET_WATCH' And '%(Value)' == '1'" /> + + + <_IsHotReloadLaunch>true + + + + + + @@ -2609,10 +2752,19 @@ global using nfloat = global::System.Runtime.InteropServices.NFloat%3B $(_R2RFrameworkName).framework.dSYM +>>>>>>> origin/net11.0 + + + <_CollectItemsForPostProcessingDependsOn> + _ComputeFrameworkFilesToPublish; + $(_CollectItemsForPostProcessingDependsOn); + + + <_ProjectLanguage>$(Language) <_ProjectLanguage Condition="'$(_ProjectLanguage)' == '' Or '$(_ProjectLanguage)' == 'C#' ">CSharp diff --git a/eng/Version.Details.props b/eng/Version.Details.props index 140fd41caa14..45dd2b5f9b4c 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -26,7 +26,7 @@ This file should be imported by eng/Versions.props 26.0.11017 26.2.10224 - 10.0.0-prerelease.25516.4 + 11.0.0-prerelease.26166.1 diff --git a/macios/Localize/loc/es/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx.lcl b/macios/Localize/loc/es/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx.lcl index a798f5eb2587..e5e8b518fb75 100644 --- a/macios/Localize/loc/es/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx.lcl +++ b/macios/Localize/loc/es/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx.lcl @@ -1150,6 +1150,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/macios/Localize/loc/pt-BR/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx.lcl b/macios/Localize/loc/pt-BR/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx.lcl index a6c7018501a8..3921d8d0ddfb 100644 --- a/macios/Localize/loc/pt-BR/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx.lcl +++ b/macios/Localize/loc/pt-BR/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx.lcl @@ -1150,6 +1150,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.cs.resx b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.cs.resx index 75351409ba5c..5c22cfdd343a 100644 --- a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.cs.resx +++ b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.cs.resx @@ -473,4 +473,15 @@ Protocol constructor overlap: + + + Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup. + + + Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources + {0} is the name of the variable, {1} is the type name. + + + Transient disposable type not declared with 'using' + \ No newline at end of file diff --git a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.de.resx b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.de.resx index 75351409ba5c..5c22cfdd343a 100644 --- a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.de.resx +++ b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.de.resx @@ -473,4 +473,15 @@ Protocol constructor overlap: + + + Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup. + + + Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources + {0} is the name of the variable, {1} is the type name. + + + Transient disposable type not declared with 'using' + \ No newline at end of file diff --git a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.es.resx b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.es.resx index 75351409ba5c..5c22cfdd343a 100644 --- a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.es.resx +++ b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.es.resx @@ -473,4 +473,15 @@ Protocol constructor overlap: + + + Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup. + + + Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources + {0} is the name of the variable, {1} is the type name. + + + Transient disposable type not declared with 'using' + \ No newline at end of file diff --git a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.fr.resx b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.fr.resx index 75351409ba5c..5c22cfdd343a 100644 --- a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.fr.resx +++ b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.fr.resx @@ -473,4 +473,15 @@ Protocol constructor overlap: + + + Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup. + + + Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources + {0} is the name of the variable, {1} is the type name. + + + Transient disposable type not declared with 'using' + \ No newline at end of file diff --git a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.it.resx b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.it.resx index 75351409ba5c..5c22cfdd343a 100644 --- a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.it.resx +++ b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.it.resx @@ -473,4 +473,15 @@ Protocol constructor overlap: + + + Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup. + + + Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources + {0} is the name of the variable, {1} is the type name. + + + Transient disposable type not declared with 'using' + \ No newline at end of file diff --git a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.ja.resx b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.ja.resx index 75351409ba5c..5c22cfdd343a 100644 --- a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.ja.resx +++ b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.ja.resx @@ -473,4 +473,15 @@ Protocol constructor overlap: + + + Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup. + + + Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources + {0} is the name of the variable, {1} is the type name. + + + Transient disposable type not declared with 'using' + \ No newline at end of file diff --git a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.ko.resx b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.ko.resx index 75351409ba5c..5c22cfdd343a 100644 --- a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.ko.resx +++ b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.ko.resx @@ -473,4 +473,15 @@ Protocol constructor overlap: + + + Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup. + + + Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources + {0} is the name of the variable, {1} is the type name. + + + Transient disposable type not declared with 'using' + \ No newline at end of file diff --git a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.pl.resx b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.pl.resx index 75351409ba5c..5c22cfdd343a 100644 --- a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.pl.resx +++ b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.pl.resx @@ -473,4 +473,15 @@ Protocol constructor overlap: + + + Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup. + + + Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources + {0} is the name of the variable, {1} is the type name. + + + Transient disposable type not declared with 'using' + \ No newline at end of file diff --git a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.pt-BR.resx b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.pt-BR.resx index 75351409ba5c..5c22cfdd343a 100644 --- a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.pt-BR.resx +++ b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.pt-BR.resx @@ -473,4 +473,15 @@ Protocol constructor overlap: + + + Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup. + + + Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources + {0} is the name of the variable, {1} is the type name. + + + Transient disposable type not declared with 'using' + \ No newline at end of file diff --git a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.ru.resx b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.ru.resx index 75351409ba5c..5c22cfdd343a 100644 --- a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.ru.resx +++ b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.ru.resx @@ -473,4 +473,15 @@ Protocol constructor overlap: + + + Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup. + + + Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources + {0} is the name of the variable, {1} is the type name. + + + Transient disposable type not declared with 'using' + \ No newline at end of file diff --git a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.tr.resx b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.tr.resx index 75351409ba5c..5c22cfdd343a 100644 --- a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.tr.resx +++ b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.tr.resx @@ -473,4 +473,15 @@ Protocol constructor overlap: + + + Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup. + + + Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources + {0} is the name of the variable, {1} is the type name. + + + Transient disposable type not declared with 'using' + \ No newline at end of file diff --git a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.zh-Hans.resx b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.zh-Hans.resx index 75351409ba5c..5c22cfdd343a 100644 --- a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.zh-Hans.resx +++ b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.zh-Hans.resx @@ -473,4 +473,15 @@ Protocol constructor overlap: + + + Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup. + + + Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources + {0} is the name of the variable, {1} is the type name. + + + Transient disposable type not declared with 'using' + \ No newline at end of file diff --git a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.zh-Hant.resx b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.zh-Hant.resx index 75351409ba5c..5c22cfdd343a 100644 --- a/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.zh-Hant.resx +++ b/macios/src/rgen/Microsoft.Macios.Bindings.Analyzer/TranslatedAssemblies/Resources.zh-Hant.resx @@ -473,4 +473,15 @@ Protocol constructor overlap: + + + Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup. + + + Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources + {0} is the name of the variable, {1} is the type name. + + + Transient disposable type not declared with 'using' + \ No newline at end of file diff --git a/msbuild/Xamarin.MacDev.Tasks/DotNetGlobals.cs b/msbuild/Xamarin.MacDev.Tasks/DotNetGlobals.cs index 0afd19463727..8d530689d7b2 100644 --- a/msbuild/Xamarin.MacDev.Tasks/DotNetGlobals.cs +++ b/msbuild/Xamarin.MacDev.Tasks/DotNetGlobals.cs @@ -4,3 +4,5 @@ global using System; global using System.Collections.Generic; global using System.Runtime.InteropServices; + +global using Xamarin.Localization.MSBuild; diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/CollectPostILTrimInformation.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/CollectPostILTrimInformation.cs new file mode 100644 index 000000000000..e97b7932721c --- /dev/null +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/CollectPostILTrimInformation.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Microsoft.Build.Framework; + +using Mono.Cecil; + +#nullable enable + +namespace Xamarin.MacDev.Tasks { + /// + /// Scans trimmed assemblies to collect information that survived trimming. + /// Designed to be extensible for collecting additional types of information in the future. + /// See docs/code/native-symbols.md for an overview of native symbol handling. + /// + public class CollectPostILTrimInformation : XamarinTask { + [Required] + public ITaskItem [] TrimmedAssemblies { get; set; } = []; + + /// + /// Output file listing the inlined dlfcn symbols that survived trimming. + /// + [Required] + public string SurvivingNativeSymbolsFile { get; set; } = ""; + + /// + /// Directory for per-assembly cache files, to avoid re-scanning unchanged assemblies. + /// + [Required] + public string CacheDirectory { get; set; } = ""; + + public override bool Execute () + { + CollectSurvivingNativeSymbols (); + return !Log.HasLoggedErrors; + } + + void CollectSurvivingNativeSymbols () + { + Directory.CreateDirectory (CacheDirectory); + + // Scan trimmed assemblies for surviving P/Invoke methods, using per-assembly caching. + var survivingSymbols = new HashSet (); + foreach (var item in TrimmedAssemblies) { + var assemblyPath = item.ItemSpec; + if (!File.Exists (assemblyPath)) + continue; + + var assemblyName = Path.GetFileNameWithoutExtension (assemblyPath); + var cacheFile = Path.Combine (CacheDirectory, assemblyName + ".dlfcn-symbols.cache"); + + string []? cachedSymbols = null; + if (File.Exists (cacheFile) && File.GetLastWriteTimeUtc (cacheFile) >= File.GetLastWriteTimeUtc (assemblyPath)) { + cachedSymbols = File.ReadAllLines (cacheFile); + Log.LogMessage (MessageImportance.Low, "Using cached dlfcn symbols for {0}", assemblyName); + } + + if (cachedSymbols is not null) { + foreach (var sym in cachedSymbols) { + if (sym.Length > 0) + survivingSymbols.Add (sym); + } + } else { + var assemblySymbols = new HashSet (); + CollectDlfcnSymbolsFromAssembly (assemblyPath, assemblySymbols); + + // Write per-assembly cache (sorted for stability). + var sortedAssemblySymbols = assemblySymbols.OrderBy (s => s).ToArray (); + File.WriteAllLines (cacheFile, sortedAssemblySymbols); + + foreach (var sym in assemblySymbols) + survivingSymbols.Add (sym); + } + } + + // Write the combined results only if contents changed (sorted for stability). + var sorted = survivingSymbols.OrderBy (s => s).ToArray (); + + if (File.Exists (SurvivingNativeSymbolsFile)) { + var existing = File.ReadAllLines (SurvivingNativeSymbolsFile); + if (existing.SequenceEqual (sorted)) + return; + } + + var dir = Path.GetDirectoryName (SurvivingNativeSymbolsFile); + if (!string.IsNullOrEmpty (dir)) + Directory.CreateDirectory (dir); + File.WriteAllLines (SurvivingNativeSymbolsFile, sorted); + Log.LogMessage (MessageImportance.Low, "Found {0} surviving inlined dlfcn symbols", survivingSymbols.Count); + } + + static void CollectDlfcnSymbolsFromAssembly (string assemblyPath, HashSet survivingSymbols) + { + const string prefix = "xamarin_Dlfcn_"; + const string suffix = "_Native"; + + using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, new ReaderParameters { ReadSymbols = false }); + foreach (var module in assembly.Modules) { + foreach (var type in module.Types) { + if (!type.HasMethods) + continue; + foreach (var method in type.Methods) { + if (!method.IsPInvokeImpl) + continue; + if (method.PInvokeInfo?.Module?.Name != "__Internal") + continue; + var name = method.Name; + if (!name.StartsWith (prefix) || !name.EndsWith (suffix)) + continue; + var symbolName = name.Substring (prefix.Length, name.Length - prefix.Length - suffix.Length); + survivingSymbols.Add (symbolName); + } + } + } + } + } +} diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/CollectUnresolvedNativeSymbols.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/CollectUnresolvedNativeSymbols.cs new file mode 100644 index 000000000000..c6543300fbaf --- /dev/null +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/CollectUnresolvedNativeSymbols.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Microsoft.Build.Framework; + +#nullable enable + +namespace Xamarin.MacDev.Tasks { + // See docs/code/native-symbols.md for an overview of native symbol handling. + public class CollectUnresolvedNativeSymbols : XamarinTask { + [Required] + public ITaskItem StaticLibrary { get; set; } = null!; + + [Required] + public string OutputFile { get; set; } = ""; + + public override bool Execute () + { + var path = StaticLibrary.ItemSpec; + if (!File.Exists (path)) { + Log.LogError ("Static library not found: {0}", path); + return false; + } + + var symbols = Xamarin.StaticLibrary.GetUnresolvedSymbols (path); + Log.LogMessage (MessageImportance.Low, "Found {0} unresolved symbols in {1}", symbols.Count, path); + + var lines = symbols.OrderBy (s => s).ToArray (); + if (File.Exists (OutputFile)) { + var existing = File.ReadAllLines (OutputFile); + if (existing.SequenceEqual (lines)) + return !Log.HasLoggedErrors; + } + + var dir = Path.GetDirectoryName (OutputFile); + if (!string.IsNullOrEmpty (dir)) + Directory.CreateDirectory (dir); + File.WriteAllLines (OutputFile, lines); + + return !Log.HasLoggedErrors; + } + } +} diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileNativeCode.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileNativeCode.cs index affd9e1f3fae..f3816db30ddb 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileNativeCode.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileNativeCode.cs @@ -136,6 +136,10 @@ public override bool Execute () arguments.Add ("-o"); arguments.Add (outputFile); + var outputDirectory = Path.GetDirectoryName (outputFile); + if (!string.IsNullOrEmpty (outputDirectory)) + Directory.CreateDirectory (outputDirectory); + arguments.Add ("-c"); arguments.Add (src); diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/ComputeNativeAOTSurvivingNativeSymbols.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/ComputeNativeAOTSurvivingNativeSymbols.cs new file mode 100644 index 000000000000..15282eb552f7 --- /dev/null +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/ComputeNativeAOTSurvivingNativeSymbols.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Microsoft.Build.Framework; + +#nullable enable + +namespace Xamarin.MacDev.Tasks { + /// + /// Takes the list of unresolved native symbols from a NativeAOT static library and computes + /// which inlined dlfcn native symbols survived trimming. The output file has the same format + /// as CollectPostILTrimInformation's surviving symbols file. + /// See docs/code/native-symbols.md for an overview of native symbol handling. + /// + public class ComputeNativeAOTSurvivingNativeSymbols : XamarinTask { + /// + /// The file listing all unresolved native symbols from the NativeAOT static library. + /// + [Required] + public string UnresolvedSymbolsFile { get; set; } = ""; + + /// + /// The full list of inlined dlfcn symbols produced by the linker step. + /// + [Required] + public string InlinedDlfcnSymbolsFile { get; set; } = ""; + + /// + /// Output file listing the native symbols that survived NativeAOT trimming. + /// + [Required] + public string SurvivingNativeSymbolsFile { get; set; } = ""; + + public override bool Execute () + { + if (!File.Exists (UnresolvedSymbolsFile) || !File.Exists (InlinedDlfcnSymbolsFile)) + return !Log.HasLoggedErrors; + + var allDlfcnSymbols = new HashSet (File.ReadAllLines (InlinedDlfcnSymbolsFile).Where (l => l.Length > 0)); + if (allDlfcnSymbols.Count == 0) + return !Log.HasLoggedErrors; + + const string prefix = "_xamarin_Dlfcn_"; + const string suffix = "_Native"; + var survivingSymbols = new HashSet (); + + foreach (var sym in File.ReadAllLines (UnresolvedSymbolsFile).Where (l => l.Length > 0)) { + if (!sym.StartsWith (prefix) || !sym.EndsWith (suffix)) + continue; + var symbolName = sym.Substring (prefix.Length, sym.Length - prefix.Length - suffix.Length); + if (allDlfcnSymbols.Contains (symbolName)) + survivingSymbols.Add (symbolName); + } + + var sorted = survivingSymbols.OrderBy (s => s).ToArray (); + + if (File.Exists (SurvivingNativeSymbolsFile)) { + var existing = File.ReadAllLines (SurvivingNativeSymbolsFile); + if (existing.SequenceEqual (sorted)) + return !Log.HasLoggedErrors; + } + + var dir = Path.GetDirectoryName (SurvivingNativeSymbolsFile); + if (!string.IsNullOrEmpty (dir)) + Directory.CreateDirectory (dir); + File.WriteAllLines (SurvivingNativeSymbolsFile, sorted); + Log.LogMessage (MessageImportance.Low, "Found {0} surviving native symbols from NativeAOT (out of {1} inlined dlfcn symbols)", survivingSymbols.Count, allDlfcnSymbols.Count); + + return !Log.HasLoggedErrors; + } + } +} diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/PostTrimmingProcessing.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/PostTrimmingProcessing.cs new file mode 100644 index 000000000000..4856d3785c4f --- /dev/null +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/PostTrimmingProcessing.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +using Microsoft.Build.Framework; + +#nullable enable + +namespace Xamarin.MacDev.Tasks { + /// + /// Performs post-trimming processing, generating native code only for symbols that survived trimming. + /// See docs/code/native-symbols.md for an overview of native symbol handling. + /// + public class PostTrimmingProcessing : XamarinTask { + /// + /// Files listing native symbols that survived trimming. Each file contains one symbol name per line. + /// These can come from either ILTrim (CollectPostILTrimInformation) or NativeAOT + /// (ComputeNativeAOTSurvivingNativeSymbols). + /// + public ITaskItem [] SurvivingNativeSymbolsFiles { get; set; } = []; + + [Required] + public string OutputDirectory { get; set; } = ""; + + [Required] + public string Architecture { get; set; } = ""; + + /// + /// Output native source files to be compiled and linked. + /// + [Output] + public ITaskItem []? NativeSourceFiles { get; set; } + + public override bool Execute () + { + var items = new List (); + + GenerateInlinedDlfcnNativeCode (items); + + NativeSourceFiles = items.ToArray (); + return !Log.HasLoggedErrors; + } + + void GenerateInlinedDlfcnNativeCode (List items) + { + // Collect all surviving symbols from all input files. + var survivingSymbols = new HashSet (); + foreach (var file in SurvivingNativeSymbolsFiles) { + var path = file.ItemSpec; + if (!File.Exists (path)) + continue; + foreach (var line in File.ReadAllLines (path)) { + if (line.Length > 0) + survivingSymbols.Add (line); + } + } + + if (survivingSymbols.Count == 0) + return; + + Directory.CreateDirectory (OutputDirectory); + var outputPath = Path.Combine (OutputDirectory, "inlined-dlfcn.c"); + + var sb = new StringBuilder (); + foreach (var field in survivingSymbols.OrderBy (s => s)) { + sb.AppendLine ($"extern void* {field};"); + sb.AppendLine ($"void* xamarin_Dlfcn_{field}_Native ();"); + sb.AppendLine ($"void* xamarin_Dlfcn_{field}_Native () {{ return &{field}; }}"); + sb.AppendLine (); + } + + var content = sb.ToString (); + if (File.Exists (outputPath) && File.ReadAllText (outputPath) == content) { + Log.LogMessage (MessageImportance.Low, "Inlined dlfcn native code is up to date"); + } else { + File.WriteAllText (outputPath, content); + Log.LogMessage (MessageImportance.Low, "Generated inlined dlfcn native code with {0} symbols", survivingSymbols.Count); + } + + var item = new Microsoft.Build.Utilities.TaskItem (outputPath); + item.SetMetadata ("Arch", Architecture.ToLowerInvariant ()); + items.Add (item); + } + } +} diff --git a/msbuild/Xamarin.MacDev.Tasks/Xamarin.MacDev.Tasks.csproj b/msbuild/Xamarin.MacDev.Tasks/Xamarin.MacDev.Tasks.csproj index 212e46bab7a8..899dfdeb2160 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Xamarin.MacDev.Tasks.csproj +++ b/msbuild/Xamarin.MacDev.Tasks/Xamarin.MacDev.Tasks.csproj @@ -140,66 +140,66 @@ Resx ResXFileCodeGenerator Errors.designer.cs - Xamarin.Bundler - Xamarin.Bundler.Errors + Xamarin.Localization.MSBuild + Xamarin.Localization.MSBuild.Errors Errors.designer.cs CSharp - Xamarin.Bundler + Xamarin.Localization.MSBuild Errors true true Errors.cs.resx - Xamarin.Bundler.Errors.cs + Xamarin.Localization.MSBuild.Errors.cs Errors.de.resx - Xamarin.Bundler.Errors.de + Xamarin.Localization.MSBuild.Errors.de Errors.es.resx - Xamarin.Bundler.Errors.es + Xamarin.Localization.MSBuild.Errors.es Errors.fr.resx - Xamarin.Bundler.Errors.fr + Xamarin.Localization.MSBuild.Errors.fr Errors.it.resx - Xamarin.Bundler.Errors.it + Xamarin.Localization.MSBuild.Errors.it Errors.ja.resx - Xamarin.Bundler.Errors.ja + Xamarin.Localization.MSBuild.Errors.ja Errors.ko.resx - Xamarin.Bundler.Errors.ko + Xamarin.Localization.MSBuild.Errors.ko Errors.pl.resx - Xamarin.Bundler.Errors.pl + Xamarin.Localization.MSBuild.Errors.pl Errors.pt-BR.resx - Xamarin.Bundler.Errors.pt-BR + Xamarin.Localization.MSBuild.Errors.pt-BR Errors.ru.resx - Xamarin.Bundler.Errors.ru + Xamarin.Localization.MSBuild.Errors.ru Errors.tr.resx - Xamarin.Bundler.Errors.tr + Xamarin.Localization.MSBuild.Errors.tr Errors.zh-Hans.resx - Xamarin.Bundler.Errors.zh-Hans + Xamarin.Localization.MSBuild.Errors.zh-Hans Errors.zh-Hant.resx - Xamarin.Bundler.Errors.zh-Hant + Xamarin.Localization.MSBuild.Errors.zh-Hant diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 957d57eece01..6cee261f5b67 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -2950,7 +2950,9 @@ Copyright (C) 2018 Microsoft. All rights reserved. - <_PostProcessingItem Include="@(_FrameworkNativeReference->'$(_AppBundleName)$(AppBundleExtension)/$(_AppFrameworksRelativePath)%(Filename)%(Extension).framework/%(Filename)%(Extension)')" Condition="'%(Kind)' == 'Framework'"> + + <_PostProcessingItem Include="@(_FilteredFrameworkToPublish->'$(_AppBundleName)$(AppBundleExtension)/$(_AppFrameworksRelativePath)%(Filename)%(Extension).framework/%(Filename)%(Extension)')"> %(Identity) @@ -2959,7 +2961,7 @@ Copyright (C) 2018 Microsoft. All rights reserved. %(Filename)%(Extension).framework.dSYM - <_PostProcessingItem Include="@(_FileNativeReference->'$(_AppBundleName)$(AppBundleExtension)/$(_AppContentsRelativePathForPostProcessing)%(Filename)%(Extension)')" Condition="'%(Kind)' == 'Dynamic'"> + <_PostProcessingItem Include="@(_FileNativeReference->'$(_AppBundleName)$(AppBundleExtension)/$(_AppContentsRelativePathForPostProcessing)%(Filename)%(Extension)')" Condition="'%(_FileNativeReference.Kind)' == 'Dynamic'"> %(Identity) diff --git a/runtime/monovm-bridge.m b/runtime/monovm-bridge.m index 6013963693da..faa1c81967cf 100644 --- a/runtime/monovm-bridge.m +++ b/runtime/monovm-bridge.m @@ -170,9 +170,9 @@ } MonoClass * -xamarin_get_nsobject_class () +xamarin_get_nsobject_class (bool allowAbsence) { - if (nsobject_class == NULL) + if (nsobject_class == NULL && !allowAbsence) xamarin_assertion_message ("Internal consistency error, please file a bug (https://github.com/dotnet/macios/issues/new). Additional data: can't get the %s class because it's been linked away.\n", "NSObject"); return nsobject_class; } @@ -225,7 +225,7 @@ bool xamarin_is_class_nsobject (MonoClass *cls) { - return mono_class_is_subclass_of (cls, xamarin_get_nsobject_class (), false); + return mono_class_is_subclass_of (cls, xamarin_get_nsobject_class (false), false); } bool diff --git a/runtime/runtime.m b/runtime/runtime.m index ab49902283b0..41a5d73f083b 100644 --- a/runtime/runtime.m +++ b/runtime/runtime.m @@ -901,7 +901,7 @@ -(struct NSObjectData*) xamarinGetNSObjectData; { gboolean rv = false; - MonoClass *nsobject_class = xamarin_get_nsobject_class (); + MonoClass *nsobject_class = xamarin_get_nsobject_class (true); if (nsobject_class) rv = cls == nsobject_class || mono_class_is_assignable_from (nsobject_class, cls); diff --git a/runtime/xamarin/runtime.h b/runtime/xamarin/runtime.h index eebdd575d66a..5323ffe95a51 100644 --- a/runtime/xamarin/runtime.h +++ b/runtime/xamarin/runtime.h @@ -279,7 +279,7 @@ MonoType * xamarin_get_nsnumber_type (); MonoType * xamarin_get_nsvalue_type (); MonoClass * xamarin_get_inativeobject_class (); MonoClass * xamarin_get_nativehandle_class (); -MonoClass * xamarin_get_nsobject_class (); +MonoClass * xamarin_get_nsobject_class (bool allowAbsence); MonoClass * xamarin_get_nsstring_class (); MonoClass * xamarin_get_runtime_class (); diff --git a/src/CoreAnimation/CAKeyFrameAnimation.cs b/src/CoreAnimation/CAKeyFrameAnimation.cs index cdeaa54a1f8b..751cc9ebaff8 100644 --- a/src/CoreAnimation/CAKeyFrameAnimation.cs +++ b/src/CoreAnimation/CAKeyFrameAnimation.cs @@ -5,14 +5,9 @@ namespace CoreAnimation { public partial class CAKeyFrameAnimation { - /// Generic type to get teh values as. - /// Returns the elements of the key frame animation as an - /// array of strongly typed values of NSObject or CoreGraphics objects. - /// - /// - /// - /// - public T [] GetValuesAs () where T : class, INativeObject + /// Returns the elements of the key frame animation as an array of strongly typed values of or CoreGraphics objects. + /// Generic type to get the values as. + public T []? GetValuesAs () where T : class, INativeObject { return NSArray.FromArrayNative (_Values); } diff --git a/src/CoreGraphics/CGFont.cs b/src/CoreGraphics/CGFont.cs index 71ab4c33e784..81294a908892 100644 --- a/src/CoreGraphics/CGFont.cs +++ b/src/CoreGraphics/CGFont.cs @@ -125,7 +125,7 @@ protected internal override void Release () // and have a unit tests to make sure this behavior does not change over time if (name is null) return null; - var nameHandle = new TransientCFString (name); + using var nameHandle = new TransientCFString (name); return Create (CGFontCreateWithFontName (nameHandle)); } diff --git a/src/Foundation/DictionaryContainer.cs b/src/Foundation/DictionaryContainer.cs index 76b82f406d5e..2ded6c0e7825 100644 --- a/src/Foundation/DictionaryContainer.cs +++ b/src/Foundation/DictionaryContainer.cs @@ -215,7 +215,7 @@ bool TryGetNativeValue (NativeHandle key, out NativeHandle value) if (!TryGetNativeValue (key, out var value)) return null; - return NSArray.ArrayFromHandleFunc (value, creator); + return NSArray.ArrayFromHandleDropNullElements (value, (v) => creator (v), NSNullBehavior.DropIfIncompatible); } /// Retrieves the array associeted with . @@ -226,7 +226,7 @@ bool TryGetNativeValue (NativeHandle key, out NativeHandle value) if (!TryGetNativeValue (key, out var value)) return null; - return NSArray.ArrayFromHandleFunc (value, (handle) => Create (handle)!); + return NSArray.ArrayFromHandleDropNullElements (value, (handle) => Create (handle)!, NSNullBehavior.DropIfIncompatible); } /// Returns the nullable associated with the specified . diff --git a/src/Foundation/NSArray.cs b/src/Foundation/NSArray.cs index e10f6c2f9fe3..4ddefa458b02 100644 --- a/src/Foundation/NSArray.cs +++ b/src/Foundation/NSArray.cs @@ -686,6 +686,18 @@ static bool TryGetItem (NativeHandle elementHandle, Converter (handle, (h) => Runtime.GetINativeObject (h, false)!, nsNullElementBehavior, releaseHandle)!; } + /// Returns a strongly-typed C# array from a handle to an NSArray, dropping null elements. + /// Parameter type, determines the kind of array returned. + /// Pointer (handle) to the unmanaged object. + /// A delegate to convert a native handle to an object of type T. + /// How to handle null and NSNull elements in the native array. + /// Whether the native NSArray instance should be released before returning or not. + /// A C# array with the values (excluding null elements). Returns if the handle is . + internal static T []? ArrayFromHandleDropNullElements (NativeHandle handle, Converter createObject, NSNullBehavior nsNullElementBehavior, bool releaseHandle = false) + { + return ArrayFromHandle (handle, createObject, nsNullElementBehavior, releaseHandle)!; + } + /// Returns a strongly-typed C# array from a handle to an NSArray, dropping null elements and guaranteeing a non-null return value. /// Parameter type, determines the kind of array returned. /// Pointer (handle) to the unmanaged object. @@ -806,68 +818,64 @@ internal static T [] ToNonNullArrayDropNullElements (NSArray? weakArray) wher GC.KeepAlive (weakArray); return rv; } -#nullable disable - /// Parameter type, determines the kind of - /// array returned, can be either an NSObject, or other - /// CoreGraphics data types. - /// Handle to an weakly typed NSArray. - /// Returns a strongly-typed C# array of the parametrized type from a weakly typed NSArray. - /// An C# array with the values. - /// - /// Use this method to get a set of NSObject arrays from an NSArray. - /// - /// Returns a strongly-typed C# array of the parametrized type from a weakly typed NSArray. + /// Parameter type, determines the kind of array returned, can be either an , or other CoreGraphics data types. + /// Handle to a weakly typed NSArray. + /// A C# array with the values. + /// + /// Use this method to get a set of NSObject arrays from an NSArray. + /// + /// (someArray); + /// var myImages = NSArray.FromArrayNative (someArray); /// ]]> - /// - /// - static public T [] FromArrayNative (NSArray weakArray) where T : class, INativeObject + /// + /// + public static T []? FromArrayNative (NSArray? weakArray) where T : class, INativeObject { - if (weakArray is null || weakArray.Handle == NativeHandle.Zero) - return null; try { - nuint n = weakArray.Count; - T [] ret = new T [n]; - for (nuint i = 0; i < n; i++) { - ret [i] = Runtime.GetINativeObject (weakArray.ValueAt (i), false); - } - return ret; + var rv = ArrayFromHandleDropNullElements (weakArray.GetHandle (), NSNullBehavior.DropIfIncompatible); + GC.KeepAlive (weakArray); + return rv; } catch { return null; } } - // Used when we need to provide our constructor - /// Parameter type, determines the kind of array returned. - /// Pointer (handle) to the unmanaged object. - /// To be added. - /// Returns a strongly-typed C# array of the parametrized type from a handle to an NSArray. - /// An C# array with the values. + /// Creates a strongly-typed C# array from a handle to an , using a custom factory function. + /// The element type for the returned array. + /// Pointer (handle) to the unmanaged object. + /// A factory function that creates an instance of from a native handle. + /// A C# array with the values, or if is . /// - /// Use this method to get a set of NSObject arrays from a handle to an NSArray. Instead of wrapping the results in NSObjects, the code invokes your method to create the return value. - /// - /// (someHandle, (x) => (int) x); + /// + /// Instead of wrapping the results in instances, + /// this method invokes for each element to create the return value. + /// + /// + /// (someHandle, (x) => (int) x); /// ]]> - /// - /// - static public T [] ArrayFromHandleFunc (NativeHandle handle, Func createObject) + /// + /// + public static T? []? ArrayFromHandleFunc (NativeHandle handle, Func createObject) { return ArrayFromHandle (handle, (v) => createObject (v)); } - /// Create a managed array from a pointer to a native NSArray instance. - /// The pointer to the native NSArray instance. - /// A callback that returns an instance of the type T for a given pointer (for an element in the NSArray). - /// Whether the native NSArray instance should be released before returning or not. - public static T [] ArrayFromHandleFunc (NativeHandle handle, Func createObject, bool releaseHandle) + /// Creates a strongly-typed C# array from a handle to an , using a custom factory function. + /// The element type for the returned array. + /// Pointer (handle) to the unmanaged object. + /// A factory function that creates an instance of from a native handle. + /// Whether the native instance should be released before returning or not. + /// A C# array with the values, or if is . + public static T? []? ArrayFromHandleFunc (NativeHandle handle, Func createObject, bool releaseHandle) { return ArrayFromHandle (handle, (v) => createObject (v), releaseHandle); } -#nullable enable /// Creates a managed array from a pointer to a native NSArray of NSDictionary objects, dropping null and NSNull elements. /// The type of objects to create from the dictionaries. /// The pointer to the native NSArray instance containing NSDictionary objects. @@ -926,21 +934,20 @@ internal static T [] NonNullDictionaryArrayFromHandleDropNullElements (Native return Runtime.GetINativeObject (val, false); } -#nullable disable - // can return an INativeObject or an NSObject - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - public T GetItem (nuint index) where T : class, INativeObject + /// Returns the element at the specified index in the , as a strongly-typed object. + /// The type to return the element as. Must be a class that implements . + /// The zero-based index of the element to retrieve. + /// The element at , or if the element cannot be converted to . + /// is greater than or equal to the array's count. + public T? GetItem (nuint index) where T : class, INativeObject { if (index >= GetCount (Handle)) - throw new ArgumentOutOfRangeException ("index"); + throw new ArgumentOutOfRangeException (nameof (index)); return UnsafeGetItem (Handle, index); } +#nullable disable /// To be added. /// To be added. /// To be added. @@ -980,6 +987,41 @@ public static NSArray From (NSObject [] [] items) } } +#nullable enable + /// Converts this to a strongly-typed C# array, dropping null and incompatible elements. + /// The element type for the returned array. Must be a class that implements . + /// A C# array of elements, excluding any null or incompatible elements. + internal T []? ToArrayDropNullElements () where T : class, INativeObject + { + var rv = ArrayFromHandleDropNullElements (Handle); + GC.KeepAlive (this); + return rv; + } + + /// Converts this to a strongly-typed C# array using a custom converter, dropping null elements. + /// The element type for the returned array. + /// A delegate to convert a native handle to an instance of . + /// A C# array of elements, excluding any null elements. + internal T []? ToArrayDropNullElements (Converter createObject) + { + var rv = ArrayFromHandleDropNullElements (Handle, createObject); + GC.KeepAlive (this); + return rv; + } + + /// Converts this to a C# array by first resolving each element to , then converting to , dropping null elements. + /// The target element type for the returned array. + /// The intermediate native object type used to convert each element. Must be a class that implements . + /// A delegate to convert an instance of to . + /// A C# array of elements, excluding any null elements. + internal T []? ToArrayDropNullElements (Converter createObject) where V : class, INativeObject + { + var rv = ArrayFromHandleDropNullElements (Handle, (handle) => createObject (Runtime.GetINativeObject (handle, false)!)); + GC.KeepAlive (this); + return rv; + } +#nullable disable + public TKey [] ToArray () where TKey : class, INativeObject { var rv = new TKey [GetCount (Handle)]; diff --git a/src/Foundation/NSArray_1.cs b/src/Foundation/NSArray_1.cs index 3aef98445aed..6ddcf0bc80ba 100644 --- a/src/Foundation/NSArray_1.cs +++ b/src/Foundation/NSArray_1.cs @@ -109,7 +109,10 @@ IEnumerator IEnumerable.GetEnumerator () } #endregion - public TKey this [nint idx] { + /// Gets the element at the specified index. + /// The zero-based index of the element to retrieve. + /// The element at , or if the element cannot be converted to . + public TKey? this [nint idx] { get { return GetItem ((nuint) idx); } diff --git a/src/Foundation/NSMutableArray_1.cs b/src/Foundation/NSMutableArray_1.cs index 7bdf359f99a1..ee8fadecd99b 100644 --- a/src/Foundation/NSMutableArray_1.cs +++ b/src/Foundation/NSMutableArray_1.cs @@ -23,6 +23,7 @@ // using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace Foundation { [SupportedOSPlatform ("ios")] @@ -193,7 +194,13 @@ public void InsertObjects (TValue [] objects, NSIndexSet atIndexes) // Additional implementations. - public TValue this [nuint index] { + /// Gets or sets the element at the specified index. + /// The zero-based index of the element to get or set. + /// The element at , or when getting if the element cannot be converted to . Setting a value is not allowed. + /// is . + /// is greater than or equal to the array's count. + [DisallowNull] // don't allow setting null values + public TValue? this [nuint index] { get { ValidateIndex (index); return GetItem (index); diff --git a/src/Foundation/NSUrlSessionConfiguration.cs b/src/Foundation/NSUrlSessionConfiguration.cs index c0f3f17eb7f7..4978e4aa0711 100644 --- a/src/Foundation/NSUrlSessionConfiguration.cs +++ b/src/Foundation/NSUrlSessionConfiguration.cs @@ -70,16 +70,17 @@ public static NSUrlSessionConfiguration CreateBackgroundSessionConfiguration (st return config; } + /// Gets or sets the proxy configurations for this session. + /// An array of objects representing the proxy configurations. [SupportedOSPlatform ("ios17.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] [SupportedOSPlatform ("tvos17.0")] public NWProxyConfig [] ProxyConfigurations { - get => NSArray.ArrayFromHandleFunc (_ProxyConfigurations, handle => new NWProxyConfig (handle, owns: false)); + get => NSArray.NonNullArrayFromHandleDropNullElements (_ProxyConfigurations, handle => new NWProxyConfig (handle, owns: false)); set { - var arr = NSArray.FromNSObjects (value); - _ProxyConfigurations = arr.Handle; - GC.KeepAlive (arr); + using var arr = NSArray.FromNSObjects (value); + _ProxyConfigurations = arr.GetHandle (); } } diff --git a/src/Makefile.generator b/src/Makefile.generator index 059b977890f1..752a49253e6e 100644 --- a/src/Makefile.generator +++ b/src/Makefile.generator @@ -4,7 +4,6 @@ # bgen.csproj.inc contains the generator_dependencies variable used to determine if the generator needs to be rebuilt or not. $(DOTNET_BUILD_DIR)/bgen.csproj.inc: export BUILD_VERBOSITY=$(DOTNET_BUILD_VERBOSITY) -$(DOTNET_BUILD_DIR)/bgen.csproj.inc: export DOTNET:=$(DOTNET) $(DOTNET_BUILD_DIR)/bgen.csproj.inc: bgen/bgen.csproj.inc $(Q) $(CP) $< $@ diff --git a/src/MediaPlayer/MPMediaQuery.cs b/src/MediaPlayer/MPMediaQuery.cs index 6f974376d383..2737a8688ab0 100644 --- a/src/MediaPlayer/MPMediaQuery.cs +++ b/src/MediaPlayer/MPMediaQuery.cs @@ -9,43 +9,47 @@ // Copyright 2011-2012, 2014-2015 Xamarin, Inc // +#if !XAMCORE_5_0 #if !TVOS && !MONOMAC +using System.ComponentModel; + #nullable enable namespace MediaPlayer { public partial class MPMediaQuery { - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Returns the media item at the specified index. + /// The zero-based index of the item to retrieve. + /// The at . + [Obsolete ("Use the 'Items' array instead.")] + [EditorBrowsable (EditorBrowsableState.Never)] public MPMediaItem GetItem (nuint index) { - using (var array = new NSArray (Messaging.IntPtr_objc_msgSend (Handle, Selector.GetHandle ("items")))) - return array.GetItem (index); + return Items! [(int) index]; } - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Returns the item section at the specified index. + /// The zero-based index of the section to retrieve. + /// The at . + [Obsolete ("Use the 'ItemSections' array instead.")] + [EditorBrowsable (EditorBrowsableState.Never)] public MPMediaQuerySection GetSection (nuint index) { - using (var array = new NSArray (Messaging.IntPtr_objc_msgSend (Handle, Selector.GetHandle ("itemSections")))) - return array.GetItem (index); + return ItemSections! [(int) index]; } - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Returns the media item collection at the specified index. + /// The zero-based index of the collection to retrieve. + /// The at . + [Obsolete ("Use the 'Collections' array instead.")] + [EditorBrowsable (EditorBrowsableState.Never)] public MPMediaItemCollection GetCollection (nuint index) { - using (var array = new NSArray (Messaging.IntPtr_objc_msgSend (Handle, Selector.GetHandle ("collections")))) - return array.GetItem (index); + return Collections! [(int) index]; } } } #endif // !TVOS +#endif // !XAMCORE_5_0 diff --git a/src/Network/NWEthernetChannel.cs b/src/Network/NWEthernetChannel.cs index c48f14253667..b99c63b04be7 100644 --- a/src/Network/NWEthernetChannel.cs +++ b/src/Network/NWEthernetChannel.cs @@ -113,7 +113,7 @@ public void Send (ReadOnlySpan content, ushort vlanTag, string remoteAddre unsafe { delegate* unmanaged trampoline = &TrampolineSendCompletion; using var block = new BlockLiteral (trampoline, callback, typeof (NWEthernetChannel), nameof (TrampolineSendCompletion)); - var remoteAddressStr = new TransientString (remoteAddress); + using var remoteAddressStr = new TransientString (remoteAddress); nw_ethernet_channel_send (GetCheckedHandle (), dispatchData.GetHandle (), vlanTag, remoteAddressStr, &block); } } diff --git a/src/ObjCRuntime/Protocol.cs b/src/ObjCRuntime/Protocol.cs index 2d711597a2e6..ca3c2bbea561 100644 --- a/src/ObjCRuntime/Protocol.cs +++ b/src/ObjCRuntime/Protocol.cs @@ -89,7 +89,7 @@ public static IntPtr GetHandle (string name) internal static IntPtr objc_getProtocol (string? name) { - var namePtr = new TransientString (name); + using var namePtr = new TransientString (name); return objc_getProtocol (namePtr); } diff --git a/src/PdfKit/PdfAnnotation.cs b/src/PdfKit/PdfAnnotation.cs index 50b403680038..a8cc8843a518 100644 --- a/src/PdfKit/PdfAnnotation.cs +++ b/src/PdfKit/PdfAnnotation.cs @@ -76,30 +76,22 @@ public PdfAnnotationKey AnnotationType { set { Type = value.GetConstant ()!; } } - /// To be added. - /// To be added. - /// To be added. + /// Gets or sets the points defining the quadrilateral bounds of the annotation. + /// An array of values representing the quadrilateral vertices, or . [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("ios")] [SupportedOSPlatform ("maccatalyst")] [SupportedOSPlatform ("tvos18.2")] - public CGPoint [] QuadrilateralPoints { + public CGPoint []? QuadrilateralPoints { get { - return NSArray.ArrayFromHandleFunc (_QuadrilateralPoints, (v) => { + return NSArray.ArrayFromHandle (_QuadrilateralPoints, (v) => { using (var value = new NSValue (v)) return value.CGPointValue; }); } set { - if (value is null) { - _QuadrilateralPoints = IntPtr.Zero; - } else { - using (var arr = new NSMutableArray ()) { - for (int i = 0; i < value.Length; i++) - arr.Add (NSValue.FromCGPoint (value [i])); - _QuadrilateralPoints = arr.Handle; - } - } + using var arr = NSArray.FromNSObjects ((element) => NSValue.FromCGPoint (element), value); + _QuadrilateralPoints = arr.GetHandle (); } } } diff --git a/src/PdfKit/PdfKit.cs b/src/PdfKit/PdfKit.cs index 76dcbdee7ebf..217aa5997465 100644 --- a/src/PdfKit/PdfKit.cs +++ b/src/PdfKit/PdfKit.cs @@ -5,52 +5,30 @@ namespace PdfKit { partial class PdfBorder { + /// Gets or sets the dash pattern for the border. + /// An array of values defining the dash pattern, or . public nfloat []? DashPattern { get { - var arr = WeakDashPattern; - if (arr is null) - return null; - var rv = new nfloat [arr.Count]; - for (uint i = 0; i < rv.Length; i++) - rv [i] = arr.GetItem (i).NFloatValue; - return rv; + return WeakDashPattern?.ToArrayDropNullElements (v => v.NFloatValue); } set { - if (value is null) { - WeakDashPattern = null; - } else { - var arr = new NSNumber [value.Length]; - for (int i = 0; i < arr.Length; i++) - arr [i] = new NSNumber (value [i]); - WeakDashPattern = NSArray.FromNSObjects (arr); - } + WeakDashPattern = NSArray.FromNSObjects ((v) => new NSNumber (v), value); } } } #if !IOS && !__TVOS__ - /// To be added. - /// To be added. + /// Represents a PDF markup annotation such as highlight, underline, or strikethrough. partial class PdfAnnotationMarkup { + /// Gets or sets the points defining the quadrilateral bounds of the markup annotation. + /// An array of values representing the quadrilateral vertices, or . public CGPoint []? QuadrilateralPoints { get { - var arr = WeakQuadrilateralPoints; - if (arr is null) - return null; - var rv = new CGPoint [arr.Count]; - for (uint i = 0; i < rv.Length; i++) - rv [i] = arr.GetItem (i).CGPointValue; - return rv; + return WeakQuadrilateralPoints?.ToArrayDropNullElements (v => v.CGPointValue); } set { - if (value is null) { - WeakQuadrilateralPoints = null; - } else { - var arr = new NSValue [value.Length]; - for (int i = 0; i < arr.Length; i++) - arr [i] = NSValue.FromCGPoint (value [i]); - WeakQuadrilateralPoints = NSArray.FromNSObjects (arr); - } + using var arr = NSArray.FromNSObjects ((v) => NSValue.FromCGPoint (v), value); + WeakQuadrilateralPoints = arr; } } } diff --git a/src/Security/SecSharedCredential.cs b/src/Security/SecSharedCredential.cs index b26510622551..b8d1ca262898 100644 --- a/src/Security/SecSharedCredential.cs +++ b/src/Security/SecSharedCredential.cs @@ -88,17 +88,33 @@ static internal class ArrayErrorActionTrampoline { [UnmanagedCallersOnly] internal static unsafe void Invoke (IntPtr block, IntPtr array, IntPtr err) { - var del = BlockLiteral.GetTarget> (block); - if (del is not null) - del (Runtime.GetNSObject (array), Runtime.GetNSObject (err)); + var del = BlockLiteral.GetTarget> (block); + if (del is not null) { + var arr = NSArray.DictionaryArrayFromHandleDropNullElements (array, (dict) => new SecSharedCredentialInfo (dict)); + del (arr, Runtime.GetNSObject (err)); + } } } - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Asynchronously requests shared web credentials from the iCloud Keychain for the specified domain and account. + /// + /// The fully qualified domain name of the website to request credentials for, + /// or to search all domains in the app's Associated Domains entitlement. + /// + /// + /// The account name to request credentials for, + /// or to request credentials for all accounts on the matching domain. + /// + /// + /// A callback invoked when the request completes, receiving an array of + /// with the matching credentials and an if the request failed. + /// + /// + /// + /// This method requires that the app has an Associated Domains entitlement configured + /// for the requested domain. The request may prompt the user for permission. + /// + /// [SupportedOSPlatform ("ios")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] @@ -107,16 +123,8 @@ internal static unsafe void Invoke (IntPtr block, IntPtr array, IntPtr err) [ObsoletedOSPlatform ("ios14.0", "Use 'ASAuthorizationPasswordRequest' instead.")] [UnsupportedOSPlatform ("tvos")] [BindingImpl (BindingImplOptions.Optimizable)] - public static void RequestSharedWebCredential (string domainName, string account, Action handler) + public static void RequestSharedWebCredential (string? domainName, string? account, Action handler) { - Action onComplete = (NSArray a, NSError e) => { - var creds = new SecSharedCredentialInfo [a.Count]; - int i = 0; - foreach (var dict in NSArray.FromArrayNative (a)) { - creds [i++] = new SecSharedCredentialInfo (dict); - } - handler (creds, e); - }; // we need to create our own block literal. using var nsDomain = (NSString?) domainName; using var nsAccount = (NSString?) account; diff --git a/src/VideoToolbox/VTCompressionProperties.cs b/src/VideoToolbox/VTCompressionProperties.cs index 3997b58bf1f3..374c7b5c0e60 100644 --- a/src/VideoToolbox/VTCompressionProperties.cs +++ b/src/VideoToolbox/VTCompressionProperties.cs @@ -332,8 +332,8 @@ public List? DataRateLimits { var list = new List (); for (nuint i = 0; i < (nuint) arr.Count; i += 2) { var rateLimit = new VTDataRateLimit ( - arr.GetItem (i).UInt32Value, - arr.GetItem (i + 1).DoubleValue + arr.GetItem (i)!.UInt32Value, + arr.GetItem (i + 1)!.DoubleValue ); list.Add (rateLimit); } diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/AnalyzerReleases.Unshipped.md b/src/rgen/Microsoft.Macios.Bindings.Analyzer/AnalyzerReleases.Unshipped.md index 9c41970e36e6..27a4c76f89d4 100644 --- a/src/rgen/Microsoft.Macios.Bindings.Analyzer/AnalyzerReleases.Unshipped.md +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/AnalyzerReleases.Unshipped.md @@ -23,3 +23,4 @@ | RBI0017 | Usage | Error | Field used with the wrong flag. | | RBI0018 | Usage | Error | The export attribute must have a nonnull selector. | | RBI0019 | Usage | Error | The selector string cannot contain any whitespace characters. | +| RBI0042 | Usage | Error | Transient disposable type not declared with 'using'. | diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.Designer.cs b/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.Designer.cs index 9d1c7f2f42cc..c860c5a7ce18 100644 --- a/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.Designer.cs +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.Designer.cs @@ -1190,5 +1190,32 @@ internal static string RBI0041Title { return ResourceManager.GetString("RBI0041Title", resourceCulture); } } + + /// + /// Looks up a localized string similar to Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup.. + /// + internal static string RBI0042Description { + get { + return ResourceManager.GetString("RBI0042Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources. + /// + internal static string RBI0042MessageFormat { + get { + return ResourceManager.GetString("RBI0042MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Transient disposable type not declared with 'using'. + /// + internal static string RBI0042Title { + get { + return ResourceManager.GetString("RBI0042Title", resourceCulture); + } + } } } diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx b/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx index bbcdc9608a49..8c9f22c65e52 100644 --- a/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx @@ -560,4 +560,17 @@ Protocol constructor overlap: + + + + Transient types (TransientString, TransientCFString, TransientCFObject) allocate native memory and must be disposed. Use the 'using' keyword to ensure proper cleanup. + + + Variable '{0}' of type '{1}' must be declared with the 'using' keyword to ensure proper disposal of native resources + {0} is the name of the variable, {1} is the type name. + + + Transient disposable type not declared with 'using' + + diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/RgenDiagnostics.cs b/src/rgen/Microsoft.Macios.Bindings.Analyzer/RgenDiagnostics.cs index 38241555d1d2..19845e2f408d 100644 --- a/src/rgen/Microsoft.Macios.Bindings.Analyzer/RgenDiagnostics.cs +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/RgenDiagnostics.cs @@ -631,4 +631,20 @@ public static class RgenDiagnostics { description: new LocalizableResourceString (nameof (Resources.RBI0041Description), Resources.ResourceManager, typeof (Resources)) ); + + /// + /// Diagnostic descriptor for when a transient disposable type (TransientString, TransientCFString, TransientCFObject) + /// is not declared with the 'using' keyword. + /// + internal static readonly DiagnosticDescriptor RBI0042 = new ( + "RBI0042", + new LocalizableResourceString (nameof (Resources.RBI0042Title), Resources.ResourceManager, typeof (Resources)), + new LocalizableResourceString (nameof (Resources.RBI0042MessageFormat), Resources.ResourceManager, + typeof (Resources)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: new LocalizableResourceString (nameof (Resources.RBI0042Description), Resources.ResourceManager, + typeof (Resources)) + ); } diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/TransientDisposableAnalyzer.cs b/src/rgen/Microsoft.Macios.Bindings.Analyzer/TransientDisposableAnalyzer.cs new file mode 100644 index 000000000000..9da4bedf3219 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/TransientDisposableAnalyzer.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using static Microsoft.Macios.Generator.RgenDiagnostics; + +namespace Microsoft.Macios.Bindings.Analyzer; + +/// +/// Analyzer to ensure that transient disposable types (TransientString, TransientCFString, TransientCFObject) +/// are always declared with the 'using' keyword to guarantee proper disposal of native resources. +/// +[DiagnosticAnalyzer (LanguageNames.CSharp)] +public class TransientDisposableAnalyzer : DiagnosticAnalyzer { + + static readonly ImmutableHashSet transientTypeFullNames = ImmutableHashSet.Create ( + "ObjCRuntime.TransientString", + "ObjCRuntime.TransientCFString", + "ObjCRuntime.TransientCFObject" + ); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create (RBI0042); + + public override void Initialize (AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis (GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution (); + context.RegisterSyntaxNodeAction (AnalyzeNode, SyntaxKind.LocalDeclarationStatement); + } + + void AnalyzeNode (SyntaxNodeAnalysisContext context) + { + if (context.Node is not LocalDeclarationStatementSyntax localDeclaration) + return; + + // If the declaration already has 'using', it's fine. + if (localDeclaration.UsingKeyword != default) + return; + + foreach (var variable in localDeclaration.Declaration.Variables) { + var symbol = context.SemanticModel.GetDeclaredSymbol (variable); + if (symbol is not ILocalSymbol localSymbol) + continue; + + var typeDisplayName = localSymbol.Type.ToDisplayString (); + if (!transientTypeFullNames.Contains (typeDisplayName)) + continue; + + var diagnostic = Diagnostic.Create (RBI0042, variable.GetLocation (), variable.Identifier.Text, localSymbol.Type.Name); + context.ReportDiagnostic (diagnostic); + } + } +} diff --git a/tests/Makefile b/tests/Makefile index 3651dc51502b..a43f5bfdc685 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -149,7 +149,6 @@ $(XHARNESS_EXECUTABLE): MSBUILD_EXE_PATH= $(XHARNESS_EXECUTABLE): $(xharness_dependencies) test.config test-system.config $(TOP)/tools/common/SdkVersions.cs $(Q_GEN) $(DOTNET) build "/bl:$@.binlog" $(MSBUILD_VERBOSITY_QUIET) xharness/xharness.csproj xharness/xharness.csproj.inc: export BUILD_VERBOSITY=$(DOTNET_BUILD_VERBOSITY) -xharness/xharness.csproj.inc: export DOTNET:=$(DOTNET) xharness/xharness.csproj.inc: export MSBUILD_EXE_PATH= killall: diff --git a/tests/cecil-tests/Documentation.KnownFailures.txt b/tests/cecil-tests/Documentation.KnownFailures.txt index cad51eed565b..e4dd16dd5a1b 100644 --- a/tests/cecil-tests/Documentation.KnownFailures.txt +++ b/tests/cecil-tests/Documentation.KnownFailures.txt @@ -20996,7 +20996,6 @@ P:Foundation.INSObjectProtocol.DebugDescription P:Foundation.INSProgressReporting.Progress P:Foundation.NSArchiveReplaceEventArgs.NewObject P:Foundation.NSArchiveReplaceEventArgs.OldObject -P:Foundation.NSArray`1.Item(System.IntPtr) P:Foundation.NSAttributedStringDocumentAttributes.Appearance P:Foundation.NSAttributedStringDocumentAttributes.Author P:Foundation.NSAttributedStringDocumentAttributes.BottomMargin @@ -21037,7 +21036,6 @@ P:Foundation.NSFileManager.TemporaryDirectory P:Foundation.NSFileManager.UserName P:Foundation.NSMetadataItem.UbiquitousItemDownloadingStatus P:Foundation.NSMorphology.Unspecified -P:Foundation.NSMutableArray`1.Item(System.UIntPtr) P:Foundation.NSMutableData.Item(System.IntPtr) P:Foundation.NSNetDomainEventArgs.Domain P:Foundation.NSNetDomainEventArgs.MoreComing @@ -21062,7 +21060,6 @@ P:Foundation.NSUbiquitousKeyValueStoreChangeEventArgs.ChangedKeys P:Foundation.NSUbiquitousKeyValueStoreChangeEventArgs.ChangeReason P:Foundation.NSUndoManagerCloseUndoGroupEventArgs.Discardable P:Foundation.NSUrlAuthenticationChallenge.SenderObject -P:Foundation.NSUrlSessionConfiguration.ProxyConfigurations P:Foundation.NSUrlSessionConfiguration.SessionType P:Foundation.NSUrlSessionConfiguration.StrongConnectionProxyDictionary P:Foundation.NSUrlSessionHandler.AllowsCellularAccess @@ -23189,8 +23186,6 @@ P:PassKit.PKVehicleConnectionSession.Delegate P:PassKit.PKVehicleConnectionSession.WeakDelegate P:PdfKit.IPdfViewDelegate.ParentViewController P:PdfKit.PdfAnnotation.ActivatableTextField -P:PdfKit.PdfAnnotationMarkup.QuadrilateralPoints -P:PdfKit.PdfBorder.DashPattern P:PdfKit.PdfDocument.AccessPermissions P:PdfKit.PdfDocument.GetClassForAnnotationClass P:PdfKit.PdfDocumentWriteOptions.AccessPermissions diff --git a/tests/common/shared-dotnet.mk b/tests/common/shared-dotnet.mk index 14f1e384b7d0..98236b97b959 100644 --- a/tests/common/shared-dotnet.mk +++ b/tests/common/shared-dotnet.mk @@ -88,9 +88,17 @@ endif ifeq ($(RID),) ifeq ($(PLATFORM),iOS) +ifeq ($(shell arch),arm64) RID=iossimulator-arm64 +else +RID=iossimulator-x64 +endif else ifeq ($(PLATFORM),tvOS) +ifeq ($(shell arch),arm64) RID=tvossimulator-arm64 +else +RID=tvossimulator-x64 +endif else ifeq ($(PLATFORM),MacCatalyst) ifeq ($(CONFIG),Release) RID=maccatalyst-x64;maccatalyst-arm64 diff --git a/tests/common/test-variations.csproj b/tests/common/test-variations.csproj index 6826b694f495..5ab16ef80b69 100644 --- a/tests/common/test-variations.csproj +++ b/tests/common/test-variations.csproj @@ -18,6 +18,8 @@ + + @@ -111,6 +113,17 @@ <_TestVariationApplied>true + + compatibility + <_TestVariationApplied>true + + + + strict + $(DefineConstants);STATIC_NATIVE_SYMBOL_LOOKUP + <_TestVariationApplied>true + + <_InvalidTestVariations Include="$(TestVariation.Split('|'))" Exclude="@(TestVariations)" /> diff --git a/tests/dotnet/HotReloadTestApp/AppDelegate.cs b/tests/dotnet/HotReloadTestApp/AppDelegate.cs new file mode 100644 index 000000000000..302179724694 --- /dev/null +++ b/tests/dotnet/HotReloadTestApp/AppDelegate.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; + +using Foundation; + +#nullable enable + +namespace HotReloadTestApp; + +public partial class Program { + + static string Variable = "Variable has not changed"; + static bool ContinueLooping = true; + + static partial void ChangeVariable (); + + + static int Main (string [] args) + { + GC.KeepAlive (typeof (NSObject)); // prevent linking away the platform assembly + + Print (0); + + for (var i = 0; i < 120 && ContinueLooping; i++) { + DoSomething (i + 1); + Thread.Sleep (TimeSpan.FromSeconds (1)); + } + + return ContinueLooping ? 1 : 0; + } + + static void DoSomething (int i) + { + ChangeVariable (); + Print (i); + } + + static string? LogPath = Environment.GetEnvironmentVariable ("HOTRELOAD_TEST_APP_LOGFILE"); + static StreamWriter? logStream; + static void Print (int number) + { + var msg = $"{number} Variable={Variable}"; + if (!string.IsNullOrEmpty (LogPath)) { + if (logStream is null) { + var fs = new FileStream (LogPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); + logStream = new StreamWriter (fs); + logStream.AutoFlush = true; + } + logStream.WriteLine (msg); + } + Console.WriteLine (msg); + } +} diff --git a/tests/dotnet/HotReloadTestApp/MacCatalyst/HotReloadTestApp.csproj b/tests/dotnet/HotReloadTestApp/MacCatalyst/HotReloadTestApp.csproj new file mode 100644 index 000000000000..6b0e2c773180 --- /dev/null +++ b/tests/dotnet/HotReloadTestApp/MacCatalyst/HotReloadTestApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-maccatalyst + + + diff --git a/tests/dotnet/HotReloadTestApp/MacCatalyst/Info.plist b/tests/dotnet/HotReloadTestApp/MacCatalyst/Info.plist new file mode 100644 index 000000000000..66daae89efcc --- /dev/null +++ b/tests/dotnet/HotReloadTestApp/MacCatalyst/Info.plist @@ -0,0 +1,8 @@ + + + + + LSUIElement + 1 + + diff --git a/tests/dotnet/HotReloadTestApp/MacCatalyst/Makefile b/tests/dotnet/HotReloadTestApp/MacCatalyst/Makefile new file mode 100644 index 000000000000..110d078f4577 --- /dev/null +++ b/tests/dotnet/HotReloadTestApp/MacCatalyst/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/HotReloadTestApp/Makefile b/tests/dotnet/HotReloadTestApp/Makefile new file mode 100644 index 000000000000..6affa45ff122 --- /dev/null +++ b/tests/dotnet/HotReloadTestApp/Makefile @@ -0,0 +1,2 @@ +TOP=../../.. +include $(TOP)/tests/common/shared-dotnet-test.mk diff --git a/tests/dotnet/HotReloadTestApp/README.md b/tests/dotnet/HotReloadTestApp/README.md new file mode 100644 index 000000000000..ed6a66f1c7b9 --- /dev/null +++ b/tests/dotnet/HotReloadTestApp/README.md @@ -0,0 +1 @@ +This is a test app for 'dotnet watch' (not for watchOS). diff --git a/tests/dotnet/HotReloadTestApp/iOS/HotReloadTestApp.csproj b/tests/dotnet/HotReloadTestApp/iOS/HotReloadTestApp.csproj new file mode 100644 index 000000000000..86d408734aa8 --- /dev/null +++ b/tests/dotnet/HotReloadTestApp/iOS/HotReloadTestApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-ios + + + diff --git a/tests/dotnet/HotReloadTestApp/iOS/Makefile b/tests/dotnet/HotReloadTestApp/iOS/Makefile new file mode 100644 index 000000000000..110d078f4577 --- /dev/null +++ b/tests/dotnet/HotReloadTestApp/iOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/HotReloadTestApp/macOS/HotReloadTestApp.csproj b/tests/dotnet/HotReloadTestApp/macOS/HotReloadTestApp.csproj new file mode 100644 index 000000000000..a77287b9ba00 --- /dev/null +++ b/tests/dotnet/HotReloadTestApp/macOS/HotReloadTestApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-macos + + + diff --git a/tests/dotnet/HotReloadTestApp/macOS/Makefile b/tests/dotnet/HotReloadTestApp/macOS/Makefile new file mode 100644 index 000000000000..110d078f4577 --- /dev/null +++ b/tests/dotnet/HotReloadTestApp/macOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/HotReloadTestApp/shared.csproj b/tests/dotnet/HotReloadTestApp/shared.csproj new file mode 100644 index 000000000000..2079dc97efe3 --- /dev/null +++ b/tests/dotnet/HotReloadTestApp/shared.csproj @@ -0,0 +1,21 @@ + + + + Exe + + HotReloadTestApp + com.xamarin.hotreloadtestapp + true + None + + true + true + + + + + + + + + diff --git a/tests/dotnet/HotReloadTestApp/shared.mk b/tests/dotnet/HotReloadTestApp/shared.mk new file mode 100644 index 000000000000..63cdf5c07a87 --- /dev/null +++ b/tests/dotnet/HotReloadTestApp/shared.mk @@ -0,0 +1,3 @@ +TOP=../../../.. +TESTNAME=HotReloadTestApp +include $(TOP)/tests/common/shared-dotnet.mk diff --git a/tests/dotnet/HotReloadTestApp/tvOS/HotReloadTestApp.csproj b/tests/dotnet/HotReloadTestApp/tvOS/HotReloadTestApp.csproj new file mode 100644 index 000000000000..bd487ddcd88d --- /dev/null +++ b/tests/dotnet/HotReloadTestApp/tvOS/HotReloadTestApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-tvos + + + diff --git a/tests/dotnet/HotReloadTestApp/tvOS/Makefile b/tests/dotnet/HotReloadTestApp/tvOS/Makefile new file mode 100644 index 000000000000..110d078f4577 --- /dev/null +++ b/tests/dotnet/HotReloadTestApp/tvOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/StaticFrameworkFilterApp/AppDelegate.cs b/tests/dotnet/StaticFrameworkFilterApp/AppDelegate.cs new file mode 100644 index 000000000000..26f5373c67a4 --- /dev/null +++ b/tests/dotnet/StaticFrameworkFilterApp/AppDelegate.cs @@ -0,0 +1,20 @@ +using System; +using System.Runtime.InteropServices; + +using Foundation; + +namespace StaticFrameworkFilterApp { + public class Program { + [DllImport ("XTest.framework/XTest")] + static extern int theUltimateAnswer (); + + static int Main (string [] args) + { + Console.WriteLine ($"Framework: {theUltimateAnswer ()}"); + + GC.KeepAlive (typeof (NSObject)); // prevent linking away the platform assembly + + return 0; + } + } +} diff --git a/tests/dotnet/StaticFrameworkFilterApp/MacCatalyst/StaticFrameworkFilterApp.csproj b/tests/dotnet/StaticFrameworkFilterApp/MacCatalyst/StaticFrameworkFilterApp.csproj new file mode 100644 index 000000000000..6b0e2c773180 --- /dev/null +++ b/tests/dotnet/StaticFrameworkFilterApp/MacCatalyst/StaticFrameworkFilterApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-maccatalyst + + + diff --git a/tests/dotnet/StaticFrameworkFilterApp/iOS/StaticFrameworkFilterApp.csproj b/tests/dotnet/StaticFrameworkFilterApp/iOS/StaticFrameworkFilterApp.csproj new file mode 100644 index 000000000000..86d408734aa8 --- /dev/null +++ b/tests/dotnet/StaticFrameworkFilterApp/iOS/StaticFrameworkFilterApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-ios + + + diff --git a/tests/dotnet/StaticFrameworkFilterApp/macOS/StaticFrameworkFilterApp.csproj b/tests/dotnet/StaticFrameworkFilterApp/macOS/StaticFrameworkFilterApp.csproj new file mode 100644 index 000000000000..a77287b9ba00 --- /dev/null +++ b/tests/dotnet/StaticFrameworkFilterApp/macOS/StaticFrameworkFilterApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-macos + + + diff --git a/tests/dotnet/StaticFrameworkFilterApp/shared.csproj b/tests/dotnet/StaticFrameworkFilterApp/shared.csproj new file mode 100644 index 000000000000..a1dca3b8773a --- /dev/null +++ b/tests/dotnet/StaticFrameworkFilterApp/shared.csproj @@ -0,0 +1,24 @@ + + + + Exe + + StaticFrameworkFilterApp + com.xamarin.staticframeworkfilter + 1.0 + + true + + + + + + + + + + + + + + diff --git a/tests/dotnet/StaticFrameworkFilterApp/tvOS/StaticFrameworkFilterApp.csproj b/tests/dotnet/StaticFrameworkFilterApp/tvOS/StaticFrameworkFilterApp.csproj new file mode 100644 index 000000000000..bd487ddcd88d --- /dev/null +++ b/tests/dotnet/StaticFrameworkFilterApp/tvOS/StaticFrameworkFilterApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-tvos + + + diff --git a/tests/dotnet/UnitTests/DotNetWatchTest.cs b/tests/dotnet/UnitTests/DotNetWatchTest.cs new file mode 100644 index 000000000000..de7d789e0400 --- /dev/null +++ b/tests/dotnet/UnitTests/DotNetWatchTest.cs @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +using Xamarin.Utils; + +namespace Xamarin.Tests { + [TestFixture] + public class DotNetWatchTest : TestBaseClass { + [Test] + [TestCase (ApplePlatform.MacOSX)] + [TestCase (ApplePlatform.MacCatalyst)] + [TestCase (ApplePlatform.iOS)] + public void DotNetWatch (ApplePlatform platform) + { + Configuration.IgnoreIfIgnoredPlatform (platform); + + var projectPath = GetProjectPath ("HotReloadTestApp", platform: platform); + Clean (projectPath); + + var projectDirectory = Path.GetDirectoryName (projectPath)!; + + var tmpdir = Cache.CreateTemporaryDirectory (); + var additionalFile = Path.Combine (tmpdir, "AdditionalFile.cs"); + + var firstContent = """ + namespace HotReloadTestApp; + public partial class Program { + static partial void ChangeVariable () + { + Variable = "Variable will change..."; + } + } + """; + + var secondContent = """ + namespace HotReloadTestApp; + public partial class Program { + static partial void ChangeVariable () + { + Variable = "Variable has changed"; + ContinueLooping = false; + } + } + """; + + File.WriteAllText (additionalFile, firstContent); + + // Debug logging is annoying here, because the test runner captures stdout/stderr, so it won't be visible until the test fails, + // which can take a while because when things go wrong here it will most likely result in timeouts. + // So instead we log to a separate file, which can be viewed as the test is running. + var debugLogPath = Path.Combine (tmpdir, "debug.log"); + using var debugLog = new StreamWriter (File.OpenWrite (debugLogPath)) { + AutoFlush = true, + }; + + var output = new List (); + var appStarted = new TaskCompletionSource (); + var waitingForChanges = new TaskCompletionSource (); + var variableChanged = new TaskCompletionSource (); + var cts = new CancellationTokenSource (); + var appOutput = new List (); + + var outputProcessor = new Action (line => { + if (line.Contains ("Variable has not changed")) { + if (appStarted.TrySetResult (true)) + debugLog.WriteLine ("Got 'Variable has not changed'"); + } + if (line.Contains ("Variable has changed")) { + if (variableChanged.TrySetResult (true)) + debugLog.WriteLine ("Got 'Variable has changed'"); + } + if (line.Contains ("Waiting for changes")) { + waitingForChanges.TrySetResult (true); + debugLog.WriteLine ("Got 'Waiting for changes'"); + } + }); + + // I'm not sure what 'dotnet watch' does with the terminal, but Console.WriteLine from the test app doesn't seem to + // reliably be captured here, so instead we have the test app write its output to a file, and we poll that file and + // process new lines as they are written. + // However, for mobile platforms, test app stdout is captured correctly, so we process both the output from the file + // and stdout we capture from 'dotnet watch' the same way, to make sure we don't miss any output. + var logPath = Path.Combine (tmpdir, "output.log"); + var pollThread = new Thread ((v) => { + for (var i = 0; i < 120; i++) { + if (File.Exists (logPath)) { + var lines = File.ReadAllLines (logPath); + Array.ForEach (lines, outputProcessor); + lock (appOutput) { + appOutput.Clear (); + appOutput.AddRange (lines); + } + } + Thread.Sleep (TimeSpan.FromSeconds (1)); + } + }) { + IsBackground = true, + }; + pollThread.Start (); + + Action outputCallback = (line) => { + debugLog.WriteLine ($"[dotnet watch] {line}"); + lock (output) { + output.Add (line); + outputProcessor (line); + } + }; + + var args = new List { + "watch", + "--non-interactive", + }; + + if (platform == ApplePlatform.iOS || platform == ApplePlatform.TVOS) { + var runtimeIdentifier = GetDefaultRuntimeIdentifier (platform); + var device = GetDeviceAsync (projectDirectory, runtimeIdentifier).GetAwaiter ().GetResult (); + debugLog.WriteLine ($"Using device: {device}"); + args.Add ($"--device={device}"); + } + + var env = new Dictionary { + { "HOTRELOAD_TEST_APP_LOGFILE", logPath }, + { "AdditionalFile", additionalFile }, + }; + + var watchTask = Execution.RunWithCallbacksAsync ( + DotNet.Executable, + args, + environment: env, + standardOutput: outputCallback, + standardError: outputCallback, + workingDirectory: projectDirectory, + timeout: TimeSpan.FromMinutes (10), + cancellationToken: cts.Token, + log: debugLog + ); + + // Wait for the app to start and show initial output + debugLog.WriteLine ("Waiting for app start..."); + if (!appStarted.Task.Wait (TimeSpan.FromMinutes (1))) + Assert.Fail ($"Timed out waiting for the app to start. Output:\n{string.Join ("\n", output)}\nDebug output:\n{string.Join ("\n", File.ReadAllLines (debugLogPath))}"); + debugLog.WriteLine ("App started!"); + + debugLog.WriteLine ("Waiting for 'dotnet watch' to be waiting for changes..."); + if (!waitingForChanges.Task.Wait (TimeSpan.FromMinutes (1))) + Assert.Fail ($"Timed out waiting for the 'dotnet watch' to be waiting for changes. Output:\n{string.Join ("\n", output)}\nDebug output:\n{string.Join ("\n", File.ReadAllLines (debugLogPath))}"); + debugLog.WriteLine ("Waiting for changes!"); + + // Write AdditionalFile.cs to trigger a rebuild via dotnet watch + File.WriteAllText (additionalFile, secondContent); + + // Wait for dotnet watch to pick up the change and the app to show the updated output + debugLog.WriteLine ("Waiting for app restart..."); + if (!variableChanged.Task.Wait (TimeSpan.FromMinutes (1))) + Assert.Fail ($"Timed out waiting for the variable to change. Output:\n{string.Join ("\n", output)}\nDebug output:\n{string.Join ("\n", File.ReadAllLines (debugLogPath))}"); + debugLog.WriteLine ("App restarted!"); + + // Cancel the watch process + debugLog.WriteLine ("Terminating the watch process..."); + cts.Cancel (); + + try { + debugLog.WriteLine ("Waiting for exit..."); + watchTask.Wait (TimeSpan.FromSeconds (30)); + debugLog.WriteLine ("Waited for exit"); + } catch { + // Expected - the process was cancelled + } + } + + // Pick any device for the specified project, and compatible with the specified runtime identifier (if provided). + // We just need any device to test that dotnet watch can detect it and deploy to it. + static async Task GetDeviceAsync (string projectDirectory, string? runtimeIdentifier = null) + { + var tmpdir = Cache.CreateTemporaryDirectory (); + var outputFile = Path.Combine (tmpdir, "AvailableDevices.json"); + var args = new List { + "build", + "-t:ComputeAvailableDevices", + "-getItem:Devices", + $"-getResultOutputFile:{outputFile}", + }; + + if (!string.IsNullOrEmpty (runtimeIdentifier)) + args.Add ($"-p:RuntimeIdentifier={runtimeIdentifier}"); + + var rv = await Execution.RunWithCallbacksAsync ( + DotNet.Executable, + args, + workingDirectory: projectDirectory, + timeout: TimeSpan.FromMinutes (1), + log: Console.Out + ); + Assert.That (rv.ExitCode, Is.EqualTo (0), "Failed to compute available devices"); + + var output = File.ReadAllText (outputFile); + var doc = JsonDocument.Parse (output); + // The devices are ordered, so that: + // * We get the same device each time, to make tests more reliable. + // * We get the most recent OS version available, to make sure we're testing on a recent OS version. + // * We get iPhones before iPads (by sorting by device type identifier), just because they take up less of the screen during a test run. + var devices = doc.RootElement.GetProperty ("Items").GetProperty ("Devices").EnumerateArray ().Select (e => { + var identity = e.GetProperty ("Identity").GetString ()!; + var osVersion = Version.Parse (e.GetProperty ("OSVersion").GetString ()!); + var deviceTypeIdentifier = e.GetProperty ("DeviceTypeIdentifier").GetString ()!; + return (Identity: identity, OsVersion: osVersion, DeviceTypeIdentifier: deviceTypeIdentifier); + }).OrderBy (d => d.OsVersion).ThenByDescending (d => d.DeviceTypeIdentifier).ThenBy (d => d.Identity).ToList (); + if (!devices.Any ()) + Assert.Inconclusive ("No devices found. Output:\n" + output); + return devices.First ().Identity; + } + } +} diff --git a/tests/dotnet/UnitTests/PostBuildTest.cs b/tests/dotnet/UnitTests/PostBuildTest.cs index df62e5a99074..c03a488b39c0 100644 --- a/tests/dotnet/UnitTests/PostBuildTest.cs +++ b/tests/dotnet/UnitTests/PostBuildTest.cs @@ -354,6 +354,43 @@ public void BundleStructureDSyms (ApplePlatform platform, string runtimeIdentifi AssertExpectedDSyms (platform, appPath); } + [Test] + [TestCase (ApplePlatform.iOS, "iossimulator-arm64")] + [TestCase (ApplePlatform.TVOS, "tvossimulator-arm64")] + [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64")] + [TestCase (ApplePlatform.MacOSX, "osx-arm64")] + public void StaticFrameworksNotInPostProcessing (ApplePlatform platform, string runtimeIdentifiers) + { + // https://github.com/dotnet/macios/issues/24840 + // This test does a Release build, which enables dsymutil/strip post-processing. + // Without the fix, the build would fail because dsymutil would try to process a + // static framework that is not present in the app bundle. + var project = "StaticFrameworkFilterApp"; + var configuration = "Release"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + + var project_path = GetProjectPath (project, runtimeIdentifiers, platform, out var appPath, configuration: configuration); + Clean (project_path); + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["Configuration"] = configuration; + // macOS and Mac Catalyst default to NoDSymUtil=true (dSYMs only generated when archiving), + // so explicitly disable it to test dSYM generation. + properties ["NoDSymUtil"] = "false"; + + var result = DotNet.AssertBuild (project_path, properties); + var postProcessingItems = GetPostProcessingItems (result.BinLogPath); + + // The dynamic framework (XTest) should be in the post-processing items + var dynamicFrameworkItems = postProcessingItems.Where (i => i.ItemSpec.Contains ("XTest.framework/XTest")).ToList (); + Assert.That (dynamicFrameworkItems.Count, Is.EqualTo (1), $"Expected 1 XTest framework post-processing item, got {dynamicFrameworkItems.Count}. All items:\n\t{string.Join ("\n\t", postProcessingItems.Select (i => i.ItemSpec))}"); + + // The static framework (XStaticArTest) should NOT be in the post-processing items, + // because it's a static library and won't be in the app bundle. + var staticFrameworkItems = postProcessingItems.Where (i => i.ItemSpec.Contains ("XStaticArTest")).ToList (); + Assert.That (staticFrameworkItems, Is.Empty, $"Static framework XStaticArTest should not be in post-processing items. All items:\n\t{string.Join ("\n\t", postProcessingItems.Select (i => i.ItemSpec))}"); + } + static List GetPostProcessingItems (string binLogPath) { var items = new Dictionary (); diff --git a/tests/monotouch-test/CoreFoundation/ProxyTest.cs b/tests/monotouch-test/CoreFoundation/ProxyTest.cs index 99c7c214d68b..3dc46ece6d40 100644 --- a/tests/monotouch-test/CoreFoundation/ProxyTest.cs +++ b/tests/monotouch-test/CoreFoundation/ProxyTest.cs @@ -18,23 +18,6 @@ namespace MonoTouchFixtures.CoreFoundation { [TestFixture] [Preserve (AllMembers = true)] public class ProxyTest { - - [Test] - public void Fields () - { - // documented but symbols are missing - // this test will fail if Apple decide to include them in the future - IntPtr lib = Dlfcn.dlopen (Constants.CoreFoundationLibrary, 0); - try { - // http://developer.apple.com/library/ios/documentation/CoreFoundation/Reference/CFProxySupport/Reference/reference.html#//apple_ref/doc/c_ref/kCFProxyAutoConfigurationHTTPResponseKey - Assert.That (Dlfcn.dlsym (lib, "kCFProxyAutoConfigurationHTTPResponseKey"), Is.EqualTo (IntPtr.Zero), "kCFProxyAutoConfigurationHTTPResponseKey"); - // http://developer.apple.com/library/ios/documentation/CoreFoundation/Reference/CFProxySupport/Reference/reference.html#//apple_ref/doc/c_ref/kCFNetworkProxiesProxyAutoConfigJavaScript - Assert.That (Dlfcn.dlsym (lib, "kCFNetworkProxiesProxyAutoConfigJavaScript"), Is.EqualTo (IntPtr.Zero), "kCFNetworkProxiesProxyAutoConfigJavaScript"); - } finally { - Dlfcn.dlclose (lib); - } - } - #if !MONOMAC HttpListener listener; int port; diff --git a/tests/monotouch-test/Foundation/AttributedStringTest.cs b/tests/monotouch-test/Foundation/AttributedStringTest.cs index 62ffbdbdd702..4844f9885cbb 100644 --- a/tests/monotouch-test/Foundation/AttributedStringTest.cs +++ b/tests/monotouch-test/Foundation/AttributedStringTest.cs @@ -52,28 +52,6 @@ void cb (NSDictionary attrs, NSRange range, ref bool stop) failEnum = true; } - [Test] - public void Fields () - { - // fields are not available in iOS (at least up to 5.1.1) - // this test will fail if this ever change in the future - IntPtr lib = Dlfcn.dlopen (Constants.FoundationLibrary, 0); - try { - Assert.That (Dlfcn.dlsym (lib, "NSFontAttributeName"), Is.EqualTo (IntPtr.Zero), "NSFontAttributeName"); - Assert.That (Dlfcn.dlsym (lib, "NSLinkAttributeName"), Is.EqualTo (IntPtr.Zero), "NSLinkAttributeName"); - Assert.That (Dlfcn.dlsym (lib, "NSUnderlineStyleAttributeName"), Is.EqualTo (IntPtr.Zero), "NSUnderlineStyleAttributeName"); - Assert.That (Dlfcn.dlsym (lib, "NSStrikethroughStyleAttributeName"), Is.EqualTo (IntPtr.Zero), "NSStrikethroughStyleAttributeName"); - Assert.That (Dlfcn.dlsym (lib, "NSStrokeWidthAttributeName"), Is.EqualTo (IntPtr.Zero), "NSStrokeWidthAttributeName"); - Assert.That (Dlfcn.dlsym (lib, "NSParagraphStyleAttributeName"), Is.EqualTo (IntPtr.Zero), "NSParagraphStyleAttributeName"); - Assert.That (Dlfcn.dlsym (lib, "NSForegroundColorAttributeName"), Is.EqualTo (IntPtr.Zero), "NSForegroundColorAttributeName"); - Assert.That (Dlfcn.dlsym (lib, "NSBackgroundColorAttributeName"), Is.EqualTo (IntPtr.Zero), "NSBackgroundColorAttributeName"); - Assert.That (Dlfcn.dlsym (lib, "NSLigatureAttributeName"), Is.EqualTo (IntPtr.Zero), "NSLigatureAttributeName"); - Assert.That (Dlfcn.dlsym (lib, "NSObliquenessAttributeName"), Is.EqualTo (IntPtr.Zero), "NSObliquenessAttributeName"); - } finally { - Dlfcn.dlclose (lib); - } - } - [Test] public void UIKitAttachmentConveniences_New () { diff --git a/tests/monotouch-test/ObjCRuntime/DlfcnTest.cs b/tests/monotouch-test/ObjCRuntime/DlfcnTest.cs index aeda262d8b76..8fa51299cbbc 100644 --- a/tests/monotouch-test/ObjCRuntime/DlfcnTest.cs +++ b/tests/monotouch-test/ObjCRuntime/DlfcnTest.cs @@ -13,6 +13,53 @@ namespace MonoTouchFixtures.ObjCRuntime { [Preserve (AllMembers = true)] public class DlfcnTest { + // These tests exercise [Field]-backed properties from Apple frameworks. + // The generated binding code calls Dlfcn.GetStringConstant / GetIntPtr / etc. + // under the hood, which is what InlineDlfcnMethodsStep transforms. + + [Test] + public void StringConstant_NSLocaleNotification () + { + var value = NSLocale.CurrentLocaleDidChangeNotification; + Assert.IsNotNull (value, "CurrentLocaleDidChangeNotification"); + Assert.AreEqual ("kCFLocaleCurrentLocaleDidChangeNotification", (string) value, "value"); + } + + [Test] + public void StringConstant_NSBundleNotification () + { + var value = NSBundle.BundleDidLoadNotification; + Assert.IsNotNull (value, "BundleDidLoadNotification"); + Assert.AreEqual ("NSBundleDidLoadNotification", (string) value, "value"); + } + + [Test] + public void StringConstant_NSUserDefaultsNotification () + { + var value = NSUserDefaults.DidChangeNotification; + Assert.IsNotNull (value, "DidChangeNotification"); + Assert.AreEqual ("NSUserDefaultsDidChangeNotification", (string) value, "value"); + } + + [Test] + public void StringConstant_NSUndoManagerNotification () + { + var value = NSUndoManager.CheckpointNotification; + Assert.IsNotNull (value, "CheckpointNotification"); + Assert.AreEqual ("NSUndoManagerCheckpointNotification", (string) value, "value"); + } + + [Test] + public void StringConstant_CachePointer () + { + // Access several string constants multiple times to test caching behavior. + // The binding code uses Dlfcn.CachePointer for repeated accesses. + for (int i = 0; i < 3; i++) { + var value = NSLocale.CurrentLocaleDidChangeNotification; + Assert.IsNotNull (value, $"iteration {i}"); + } + } + [Test] public void OpenClose_libSystem () { @@ -32,7 +79,7 @@ public void OpenClose_libSystem () [Test] public void GetVariables () { - var symbol = "x_native_field"; + const string symbol = "x_native_field"; var handle = (IntPtr) Dlfcn.RTLD.Default; Assert.AreNotEqual (IntPtr.Zero, Dlfcn.dlsym (handle, symbol), "Symbol"); @@ -63,8 +110,10 @@ public void GetVariables () Assert.AreEqual (-3.9541907E+28f, Dlfcn.GetStruct (handle, symbol), "GetStruct"); Assert.AreEqual (-7.7576533930025207E-103d, Dlfcn.GetStruct (handle, symbol), "GetStruct"); +#if !STATIC_NATIVE_SYMBOL_LOOKUP Assert.AreEqual ((ulong) 0, Dlfcn.GetStruct (handle, "inexistent_symbol"), "GetStruct inexistent"); Assert.AreEqual ((ulong) 0, Dlfcn.GetStruct (handle, "inexistent_symbol").Value, "GetStruct inexistent"); +#endif Dlfcn.SetInt16 (handle, symbol, 0x77); Assert.AreEqual ((short) 0x77, Dlfcn.GetInt16 (handle, symbol), "SetInt16"); @@ -122,5 +171,24 @@ struct SomeValue { public ulong Value; } #pragma warning restore CS0649 + + [Test] + public void FieldProperty_CGRect () + { + Assert.Multiple (() => { + // CGRect.Null is backed by [Field("CGRectNull")] which calls Dlfcn.GetCGRect. + var value = global::CoreGraphics.CGRect.Null; + Assert.That (value.X, Is.EqualTo (nfloat.PositiveInfinity), "CGRectNull.X"); + Assert.That (value.Y, Is.EqualTo (nfloat.PositiveInfinity), "CGRectNull.Y"); + Assert.That (value.Width, Is.EqualTo ((nfloat) 0), "CGRectNull.Width"); + Assert.That (value.Height, Is.EqualTo ((nfloat) 0), "CGRectNull.Height"); + + var infinite = global::CoreGraphics.CGRect.Infinite; + Assert.That (infinite.X, Is.EqualTo (nfloat.MinValue / 2), "CGRectInfinite.X"); + Assert.That (infinite.Y, Is.EqualTo (nfloat.MinValue / 2), "CGRectInfinite.Y"); + Assert.That (infinite.Width, Is.EqualTo (nfloat.MaxValue), "CGRectInfinite.Width"); + Assert.That (infinite.Height, Is.EqualTo (nfloat.MaxValue), "CGRectInfinite.Height"); + }); + } } } diff --git a/tests/monotouch-test/Security/RecordTest.cs b/tests/monotouch-test/Security/RecordTest.cs index 79e6aa29f7ee..af4a398bb6d4 100644 --- a/tests/monotouch-test/Security/RecordTest.cs +++ b/tests/monotouch-test/Security/RecordTest.cs @@ -310,29 +310,38 @@ public void DeskCase_83099_InmutableDictionary () { var testUsername = "testusername"; - //TEST 1: Save a keychain value - var test1 = SaveUserPassword (testUsername, "testValue1", out var queryCode, out var addCode, out var updateCode); - Assert.IsTrue (test1, $"Password could not be saved to keychain. queryCode: {queryCode} addCode: {addCode} updateCode: {updateCode}"); + // Clean up any stale keychain entries from previous test runs to avoid + // the keychain returning ItemNotFound on query but DuplicateItem on add. + ForceRemoveUserPassword (testUsername); - //TEST 2: Get the saved keychain value - var test2 = GetUserPassword (testUsername); - Assert.IsTrue (StringUtil.StringsEqual (test2, "testValue1", false)); + try { + //TEST 1: Save a keychain value + var test1 = SaveUserPassword (testUsername, "testValue1", out var queryCode, out var addCode, out var updateCode); + Assert.IsTrue (test1, $"Password could not be saved to keychain. queryCode: {queryCode} addCode: {addCode} updateCode: {updateCode}"); + + //TEST 2: Get the saved keychain value + var test2 = GetUserPassword (testUsername); + Assert.IsTrue (StringUtil.StringsEqual (test2, "testValue1", false)); - //TEST 3: Update the keychain value - var test3 = SaveUserPassword (testUsername, "testValue2", out queryCode, out addCode, out updateCode); - Assert.IsTrue (test3, "Password could not be saved to keychain. queryCode: {queryCode} addCode: {addCode} updateCode: {updateCode}"); + //TEST 3: Update the keychain value + var test3 = SaveUserPassword (testUsername, "testValue2", out queryCode, out addCode, out updateCode); + Assert.IsTrue (test3, $"Password could not be saved to keychain. queryCode: {queryCode} addCode: {addCode} updateCode: {updateCode}"); - //TEST 4: Get the updated keychain value - var test4 = GetUserPassword (testUsername); - Assert.IsTrue (StringUtil.StringsEqual (test4, "testValue2", false)); + //TEST 4: Get the updated keychain value + var test4 = GetUserPassword (testUsername); + Assert.IsTrue (StringUtil.StringsEqual (test4, "testValue2", false)); - //TEST 5: Clear the keychain values - var test5 = ClearUserPassword (testUsername); - Assert.IsTrue (test5, "Password could not be cleared from keychain"); + //TEST 5: Clear the keychain values + var test5 = ClearUserPassword (testUsername); + Assert.IsTrue (test5, "Password could not be cleared from keychain"); - //TEST 6: Verify no keychain value - var test6 = GetUserPassword (testUsername); - Assert.IsNull (test6, "No password should exist here"); + //TEST 6: Verify no keychain value + var test6 = GetUserPassword (testUsername); + Assert.IsNull (test6, "No password should exist here"); + } finally { + // Always clean up to avoid leaving stale entries for subsequent runs + ForceRemoveUserPassword (testUsername); + } } public static string GetUserPassword (string username) @@ -367,6 +376,13 @@ record = CreateSecRecord (SecKind.InternetPassword, ); addCode = SecKeyChain.Add (record); success = (addCode == SecStatusCode.Success); + // Handle inconsistent keychain state: query returned ItemNotFound + // but add returned DuplicateItem. Force-remove and retry. + if (addCode == SecStatusCode.DuplicateItem) { + SecKeyChain.Remove (searchRecord); + addCode = SecKeyChain.Add (record); + success = (addCode == SecStatusCode.Success); + } } if (queryCode == SecStatusCode.Success && record is not null) { record.ValueData = NSData.FromString (password); @@ -376,6 +392,15 @@ record = CreateSecRecord (SecKind.InternetPassword, return success; } + public static void ForceRemoveUserPassword (string username) + { + var searchRecord = CreateSecRecord (SecKind.InternetPassword, + server: "Test1", + account: username.ToLower () + ); + SecKeyChain.Remove (searchRecord); + } + public static bool ClearUserPassword (string username) { var success = false; diff --git a/tests/monotouch-test/Security/SecSharedCredentialTest.cs b/tests/monotouch-test/Security/SecSharedCredentialTest.cs index 7a3e4afd8d24..f1bf50ba6905 100644 --- a/tests/monotouch-test/Security/SecSharedCredentialTest.cs +++ b/tests/monotouch-test/Security/SecSharedCredentialTest.cs @@ -19,9 +19,6 @@ public class SecSharedCredentialTest { [SetUp] public void SetUp () { - // The API here was introduced to Mac Catalyst later than for the other frameworks, so we have this additional check - TestRuntime.AssertSystemVersion (ApplePlatform.MacCatalyst, 14, 0, throwIfOtherPlatform: false); - domainName = "com.xamarin.monotouch-test"; account = "twitter"; password = "12345678"; @@ -52,8 +49,6 @@ public void AddSharedWebCredentialNullAccount () [Timeout (5000)] public void AddSharedWebCredentialNotNullPassword () { - TestRuntime.AssertSystemVersion (ApplePlatform.iOS, 8, 0, throwIfOtherPlatform: false); - Action handler = (NSError e) => { // we do nothing, if we did block the test should be interactive because a dialog is shown. }; @@ -66,8 +61,6 @@ public void AddSharedWebCredentialNotNullPassword () [Timeout (5000)] public void AddSharedWebCredentialNullPassword () { - TestRuntime.AssertSystemVersion (ApplePlatform.iOS, 8, 0, throwIfOtherPlatform: false); - password = null; Action handler = (NSError e) => { // we do nothing, if we did block the test should be interactive because a dialog is shown. @@ -76,10 +69,32 @@ public void AddSharedWebCredentialNullPassword () } [Test] - public void CreateSharedWebCredentialPassword () + // We do not want to block for a long period of time if the event is not set. + // We are testing the fact that the trampoline works. + [Timeout (5000)] + public void RequestSharedWebCredentialTest () + { + Action handler = (SecSharedCredentialInfo [] creds, NSError e) => { + // we do nothing, if we did block the test should be interactive because a dialog is shown. + }; + SecSharedCredential.RequestSharedWebCredential (domainName, account, handler); + } + + [Test] + // We do not want to block for a long period of time if the event is not set. + // We are testing the fact that the trampoline works. + [Timeout (5000)] + public void RequestSharedWebCredentialNullDomainAndAccountTest () { - TestRuntime.AssertSystemVersion (ApplePlatform.iOS, 8, 0, throwIfOtherPlatform: false); + Action handler = (SecSharedCredentialInfo [] creds, NSError e) => { + // we do nothing, if we did block the test should be interactive because a dialog is shown. + }; + SecSharedCredential.RequestSharedWebCredential (null, null, handler); + } + [Test] + public void CreateSharedWebCredentialPassword () + { var pwd = SecSharedCredential.CreateSharedWebCredentialPassword (); Assert.IsNotNull (pwd); } diff --git a/tests/monotouch-test/System.Net.Http/NSUrlSessionHandlerTest.cs b/tests/monotouch-test/System.Net.Http/NSUrlSessionHandlerTest.cs index 628c45094e57..34e018063694 100644 --- a/tests/monotouch-test/System.Net.Http/NSUrlSessionHandlerTest.cs +++ b/tests/monotouch-test/System.Net.Http/NSUrlSessionHandlerTest.cs @@ -59,6 +59,24 @@ public void DisposeAndRecreateBackgroundSessionHandler () IgnoreIfExceptionDueToBackgroundServiceInUseByAnotherProcess (ex); TestRuntime.IgnoreInCIIfBadNetwork (ex); + if (ex is ObjCException && ex.ToString ().Contains ("Task created in a session that has been invalidated")) { + // When disposing an NSUrlSessionHandler backed by a background NSUrlSession + // and immediately creating a new handler with the same background session + // identifier, the new session can fail with 'Task created in a session + // that has been invalidated'. + // + // This happens because InvalidateAndCancel() is asynchronous - it marks + // the session for invalidation but doesn't wait for it to complete. Apple + // reuses the same native session object for background sessions with the + // same identifier, so creating a new session before invalidation completes + // returns the already-invalidated session. + // + // There are a couple of fixes: + // * Add a Thread.Sleep before creating the second NSUrlSessionHandler - but this will slow down every test run, + // * Wait for the session to become invalid in NSUrlSessionHandler (add a 'DidBecomeInvalid' implementation, and wait for that in Dispose) - which may unnecessarily slow down working code. + // * Detect this scenario here, and just mark the test as inconclusive. The test does something somewhat unusual (create two background sessions with the same identifier in quick succession), so this seems like the best approach for now. + Assert.Inconclusive ("The previous background session wasn't fully invalidated before we tried to create a new background session (with the same identifier)"); + } Assert.IsNull (ex, "Second request exception"); } diff --git a/tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/TransientDisposableAnalyzerTests.cs b/tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/TransientDisposableAnalyzerTests.cs new file mode 100644 index 000000000000..81bd6480bdb4 --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/TransientDisposableAnalyzerTests.cs @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Xamarin.Tests; +using Xamarin.Utils; +using Xunit; + +namespace Microsoft.Macios.Bindings.Analyzer.Tests; + +public class TransientDisposableAnalyzerTests : BaseGeneratorWithAnalyzerTestClass { + class ErrorTestCases : IEnumerable { + public IEnumerator GetEnumerator () + { + // TransientString without using + yield return [ + """ + using System; + + namespace ObjCRuntime { + struct TransientString : IDisposable { + public void Dispose () { } + } + } + + class Test + { + void Method () + { + var s = new ObjCRuntime.TransientString (); + } + } + """]; + + // TransientCFString without using + yield return [ + """ + using System; + + namespace ObjCRuntime { + ref struct TransientCFString { + public void Dispose () { } + } + } + + class Test + { + void Method () + { + ObjCRuntime.TransientCFString s = new ObjCRuntime.TransientCFString (); + } + } + """]; + + // TransientCFObject without using + yield return [ + """ + using System; + + namespace ObjCRuntime { + ref struct TransientCFObject { + public void Dispose () { } + } + } + + class Test + { + void Method () + { + var obj = new ObjCRuntime.TransientCFObject (); + } + } + """]; + } + + IEnumerator IEnumerable.GetEnumerator () => GetEnumerator (); + } + + class NoErrorTestCases : IEnumerable { + public IEnumerator GetEnumerator () + { + // TransientString with using var + yield return [ + """ + using System; + + namespace ObjCRuntime { + struct TransientString : IDisposable { + public void Dispose () { } + } + } + + class Test + { + void Method () + { + using var s = new ObjCRuntime.TransientString (); + } + } + """]; + + // TransientCFString with using var + yield return [ + """ + using System; + + namespace ObjCRuntime { + ref struct TransientCFString { + public void Dispose () { } + } + } + + class Test + { + void Method () + { + using var s = new ObjCRuntime.TransientCFString (); + } + } + """]; + + // TransientCFObject with using var + yield return [ + """ + using System; + + namespace ObjCRuntime { + ref struct TransientCFObject { + public void Dispose () { } + } + } + + class Test + { + void Method () + { + using var obj = new ObjCRuntime.TransientCFObject (); + } + } + """]; + + // Non-transient type without using is fine + yield return [ + """ + using System; + + struct SomeOtherStruct : IDisposable { + public void Dispose () { } + } + + class Test + { + void Method () + { + var s = new SomeOtherStruct (); + } + } + """]; + + // User-defined TransientString outside ObjCRuntime namespace should not trigger + yield return [ + """ + using System; + + struct TransientString : IDisposable { + public void Dispose () { } + } + + class Test + { + void Method () + { + var s = new TransientString (); + } + } + """]; + } + + IEnumerator IEnumerable.GetEnumerator () => GetEnumerator (); + } + + [Theory] + [AllSupportedPlatformsClassData] + public async Task TransientWithoutUsingTests (ApplePlatform platform, string inputText) + { + var (compilation, _) = CreateCompilation (platform, sources: inputText); + var diagnostics = await RunAnalyzer (new TransientDisposableAnalyzer (), compilation); + var analyzerDiagnostics = diagnostics.Where (d => d.Id == "RBI0042").ToArray (); + Assert.True (analyzerDiagnostics.Length != 0); + } + + [Theory] + [AllSupportedPlatformsClassData] + public async Task TransientWithUsingTests (ApplePlatform platform, string inputText) + { + var (compilation, _) = CreateCompilation (platform, sources: inputText); + var diagnostics = await RunAnalyzer (new TransientDisposableAnalyzer (), compilation); + var analyzerDiagnostics = diagnostics.Where (d => d.Id == "RBI0042").ToArray (); + Assert.Empty (analyzerDiagnostics); + } +} diff --git a/tests/sharpie/Sharpie.Bind.Tests/ObjectiveCClass.cs b/tests/sharpie/Sharpie.Bind.Tests/ObjectiveCClass.cs index 06946f8eee59..206685e05ae6 100644 --- a/tests/sharpie/Sharpie.Bind.Tests/ObjectiveCClass.cs +++ b/tests/sharpie/Sharpie.Bind.Tests/ObjectiveCClass.cs @@ -627,4 +627,130 @@ interface GoodClass { """; bindings.AssertSuccess (expectedBindings); } + + [Test] + public void DeepSplit_SplitsPerHeader () + { + // Verify that --deepsplit creates one .cs file per source header. + var binder = new BindTool (); + var tmpdir = Cache.CreateTemporaryDirectory (); + var headersDir = Path.Combine (tmpdir, "headers"); + Directory.CreateDirectory (headersDir); + + File.WriteAllText (Path.Combine (headersDir, "ClassA.h"), + """ + @interface ClassA { + } + @property int valueA; + @end + """); + + File.WriteAllText (Path.Combine (headersDir, "ClassB.h"), + """ + @interface ClassB { + } + @property int valueB; + @end + """); + + var mainHeader = Path.Combine (tmpdir, "main.h"); + File.WriteAllText (mainHeader, $"#import \"{Path.Combine (headersDir, "ClassA.h")}\"\n#import \"{Path.Combine (headersDir, "ClassB.h")}\"\n"); + + binder.SourceFile = mainHeader; + binder.DirectoriesInScope.Add (headersDir); + binder.OutputDirectory = tmpdir; + binder.DeepSplit = true; + Configuration.IgnoreIfIgnoredPlatform (binder.Platform); + binder.PlatformAssembly = Extensions.GetPlatformAssemblyPath (binder.Platform); + binder.ClangResourceDirectory = Extensions.GetClangResourceDirectory (); + var bindings = binder.BindInOrOut (); + bindings.AssertSuccess (null); + + Assert.That (bindings.AdditionalFiles.ContainsKey ("ClassA.cs"), Is.True, "Should have ClassA.cs"); + Assert.That (bindings.AdditionalFiles.ContainsKey ("ClassB.cs"), Is.True, "Should have ClassB.cs"); + Assert.That (bindings.AdditionalFiles ["ClassA.cs"], Does.Contain ("ClassA"), "ClassA.cs should contain ClassA"); + Assert.That (bindings.AdditionalFiles ["ClassA.cs"], Does.Not.Contain ("ClassB"), "ClassA.cs should not contain ClassB"); + Assert.That (bindings.AdditionalFiles ["ClassB.cs"], Does.Contain ("ClassB"), "ClassB.cs should contain ClassB"); + Assert.That (bindings.AdditionalFiles ["ClassB.cs"], Does.Not.Contain ("ClassA"), "ClassB.cs should not contain ClassA"); + } + + [Test] + public void DeepSplit_StructsAndEnumsSeparate () + { + // Verify that structs and enums go into StructsAndEnums.cs even in deepsplit mode. + var binder = new BindTool (); + var tmpdir = Cache.CreateTemporaryDirectory (); + var headersDir = Path.Combine (tmpdir, "headers"); + Directory.CreateDirectory (headersDir); + + File.WriteAllText (Path.Combine (headersDir, "Widget.h"), + """ + struct WidgetSize { + int width; + int height; + }; + @interface Widget { + } + @property int tag; + @end + """); + + var mainHeader = Path.Combine (tmpdir, "main.h"); + File.WriteAllText (mainHeader, $"#import \"{Path.Combine (headersDir, "Widget.h")}\"\n"); + + binder.SourceFile = mainHeader; + binder.DirectoriesInScope.Add (headersDir); + binder.OutputDirectory = tmpdir; + binder.DeepSplit = true; + Configuration.IgnoreIfIgnoredPlatform (binder.Platform); + binder.PlatformAssembly = Extensions.GetPlatformAssemblyPath (binder.Platform); + binder.ClangResourceDirectory = Extensions.GetClangResourceDirectory (); + var bindings = binder.BindInOrOut (); + bindings.AssertSuccess (null); + + Assert.That (bindings.AdditionalFiles.ContainsKey ("Widget.cs"), Is.True, "Should have Widget.cs for the interface"); + Assert.That (bindings.AdditionalFiles.ContainsKey ("StructsAndEnums.cs"), Is.True, "Should have StructsAndEnums.cs for the struct"); + Assert.That (bindings.AdditionalFiles ["Widget.cs"], Does.Contain ("Widget"), "Widget.cs should contain Widget interface"); + Assert.That (bindings.AdditionalFiles ["Widget.cs"], Does.Not.Contain ("WidgetSize"), "Widget.cs should not contain the struct"); + Assert.That (bindings.AdditionalFiles ["StructsAndEnums.cs"], Does.Contain ("WidgetSize"), "StructsAndEnums.cs should contain the struct"); + } + + [Test] + public void DeepSplit_MultipleClassesInOneHeader () + { + // Verify that multiple classes from the same header go into the same .cs file. + var binder = new BindTool (); + var tmpdir = Cache.CreateTemporaryDirectory (); + var headersDir = Path.Combine (tmpdir, "headers"); + Directory.CreateDirectory (headersDir); + + File.WriteAllText (Path.Combine (headersDir, "Models.h"), + """ + @interface Person { + } + @property int age; + @end + @interface Car { + } + @property int speed; + @end + """); + + var mainHeader = Path.Combine (tmpdir, "main.h"); + File.WriteAllText (mainHeader, $"#import \"{Path.Combine (headersDir, "Models.h")}\"\n"); + + binder.SourceFile = mainHeader; + binder.DirectoriesInScope.Add (headersDir); + binder.OutputDirectory = tmpdir; + binder.DeepSplit = true; + Configuration.IgnoreIfIgnoredPlatform (binder.Platform); + binder.PlatformAssembly = Extensions.GetPlatformAssemblyPath (binder.Platform); + binder.ClangResourceDirectory = Extensions.GetClangResourceDirectory (); + var bindings = binder.BindInOrOut (); + bindings.AssertSuccess (null); + + Assert.That (bindings.AdditionalFiles.ContainsKey ("Models.cs"), Is.True, "Should have Models.cs"); + Assert.That (bindings.AdditionalFiles ["Models.cs"], Does.Contain ("Person"), "Models.cs should contain Person"); + Assert.That (bindings.AdditionalFiles ["Models.cs"], Does.Contain ("Car"), "Models.cs should contain Car"); + } } diff --git a/tests/sharpie/Tests/Massagers/PlatformTypeMapping.h b/tests/sharpie/Tests/Massagers/PlatformTypeMapping.h index 4ab1a8a53daf..ef789bba8c0c 100644 --- a/tests/sharpie/Tests/Massagers/PlatformTypeMapping.h +++ b/tests/sharpie/Tests/Massagers/PlatformTypeMapping.h @@ -9,4 +9,5 @@ @interface WebFetcher : NSObject @property (nonatomic, readonly, copy) NSURL *url; -(NSURLResponse *)getResponseForUrl:(NSURL *)url withCredential:(NSURLCredential *)credential; +-(void)loadDataWithUrl:(NSURL *)url completionHandler:(void (^)(NSData *, NSURLResponse *))handler; @end diff --git a/tests/sharpie/Tests/Massagers/PlatformTypeMapping.iphoneos.cs b/tests/sharpie/Tests/Massagers/PlatformTypeMapping.iphoneos.cs index bd219fe54ae0..33be4e000f76 100644 --- a/tests/sharpie/Tests/Massagers/PlatformTypeMapping.iphoneos.cs +++ b/tests/sharpie/Tests/Massagers/PlatformTypeMapping.iphoneos.cs @@ -1,3 +1,4 @@ +using System; using Foundation; using ObjCRuntime; @@ -11,4 +12,8 @@ interface WebFetcher : INSUrlConnectionDelegate { // -(NSURLResponse *)getResponseForUrl:(NSURL *)url withCredential:(NSURLCredential *)credential; [Export ("getResponseForUrl:withCredential:")] NSUrlResponse GetResponseForUrl (NSUrl url, NSUrlCredential credential); + + // -(void)loadDataWithUrl:(NSURL *)url completionHandler:(void (^)(NSData *, NSURLResponse *))handler; + [Export ("loadDataWithUrl:completionHandler:")] + void LoadDataWithUrl (NSUrl url, Action handler); } diff --git a/tests/sharpie/Tests/Massagers/PlatformTypeMapping.macosx.cs b/tests/sharpie/Tests/Massagers/PlatformTypeMapping.macosx.cs index bd219fe54ae0..33be4e000f76 100644 --- a/tests/sharpie/Tests/Massagers/PlatformTypeMapping.macosx.cs +++ b/tests/sharpie/Tests/Massagers/PlatformTypeMapping.macosx.cs @@ -1,3 +1,4 @@ +using System; using Foundation; using ObjCRuntime; @@ -11,4 +12,8 @@ interface WebFetcher : INSUrlConnectionDelegate { // -(NSURLResponse *)getResponseForUrl:(NSURL *)url withCredential:(NSURLCredential *)credential; [Export ("getResponseForUrl:withCredential:")] NSUrlResponse GetResponseForUrl (NSUrl url, NSUrlCredential credential); + + // -(void)loadDataWithUrl:(NSURL *)url completionHandler:(void (^)(NSData *, NSURLResponse *))handler; + [Export ("loadDataWithUrl:completionHandler:")] + void LoadDataWithUrl (NSUrl url, Action handler); } diff --git a/tests/sharpie/Tests/ObjCGenerics.iphoneos.cs b/tests/sharpie/Tests/ObjCGenerics.iphoneos.cs index a11273618fde..63e042105327 100644 --- a/tests/sharpie/Tests/ObjCGenerics.iphoneos.cs +++ b/tests/sharpie/Tests/ObjCGenerics.iphoneos.cs @@ -6,16 +6,16 @@ interface CNLabeledValue : INSCopying, INSSecureCoding { // @property (readonly, copy, nonatomic) ValueType ValueTypeProperty; [Export ("ValueTypeProperty", ArgumentSemantic.Copy)] - NSObject ValueTypeProperty { get; } + NSObject ValueTypeProperty { get; } // -(ValueType _Nullable)getValueTypeMethod; [NullAllowed, Export ("getValueTypeMethod")] [Verify (MethodToProperty)] - NSObject ValueTypeMethod { get; } + NSObject ValueTypeMethod { get; } // -(void)setValueTypeMethod:(ValueType _Nullable)obj; [Export ("setValueTypeMethod:")] - void SetValueTypeMethod ([NullAllowed] NSObject obj); + void SetValueTypeMethod ([NullAllowed] NSObject obj); } // @protocol A diff --git a/tests/sharpie/Tests/ObjCGenerics.macosx.cs b/tests/sharpie/Tests/ObjCGenerics.macosx.cs index a11273618fde..63e042105327 100644 --- a/tests/sharpie/Tests/ObjCGenerics.macosx.cs +++ b/tests/sharpie/Tests/ObjCGenerics.macosx.cs @@ -6,16 +6,16 @@ interface CNLabeledValue : INSCopying, INSSecureCoding { // @property (readonly, copy, nonatomic) ValueType ValueTypeProperty; [Export ("ValueTypeProperty", ArgumentSemantic.Copy)] - NSObject ValueTypeProperty { get; } + NSObject ValueTypeProperty { get; } // -(ValueType _Nullable)getValueTypeMethod; [NullAllowed, Export ("getValueTypeMethod")] [Verify (MethodToProperty)] - NSObject ValueTypeMethod { get; } + NSObject ValueTypeMethod { get; } // -(void)setValueTypeMethod:(ValueType _Nullable)obj; [Export ("setValueTypeMethod:")] - void SetValueTypeMethod ([NullAllowed] NSObject obj); + void SetValueTypeMethod ([NullAllowed] NSObject obj); } // @protocol A diff --git a/tests/xharness/Jenkins/TestVariationsFactory.cs b/tests/xharness/Jenkins/TestVariationsFactory.cs index 50f4c27c94b9..b88bf27d4fdc 100644 --- a/tests/xharness/Jenkins/TestVariationsFactory.cs +++ b/tests/xharness/Jenkins/TestVariationsFactory.cs @@ -128,6 +128,9 @@ IEnumerable GetTestData (RunTestTask test) yield return new TestData { Variation = "Debug (interpreter)", TestVariation = "interpreter", Debug = true, Ignored = ignore }; yield return new TestData { Variation = "Release (interpreter)", TestVariation = "interpreter", Debug = false, Ignored = ignore, UseLlvm = false }; } + yield return new TestData { Variation = $"Release (compat inline dlfcn)", TestVariation = "inline-dlfcn-methods-compat|release", Debug = false, Ignored = ignore }; + yield return new TestData { Variation = $"Release (strict inline dlfcn)", TestVariation = "inline-dlfcn-methods-strict|release", Debug = false, Ignored = ignore }; + yield return new TestData { Variation = $"Release (NativeAOT, .NET 11 defaults)", TestVariation = "inline-dlfcn-methods-strict|nativeaot|release", PublishAot = true, Debug = false, Ignored = ignore, LinkMode = "Full" }; break; case "introspection": if (mac_supports_arm64) diff --git a/tools/common/Frameworks.cs b/tools/common/Frameworks.cs index 07b8b744a921..77eafcf1a993 100644 --- a/tools/common/Frameworks.cs +++ b/tools/common/Frameworks.cs @@ -405,7 +405,7 @@ public static Frameworks CreateiOSFrameworks (bool is_simulator_build) { "CoreNFC", "CoreNFC", new Version (11, 0), new Version (15, 0), true }, /* not always present, e.g. iPad w/iOS 12, so must be weak linked; doesn't work in the simulator in Xcode 12 (https://stackoverflow.com/q/63915728/183422), but works in at least Xcode 15 (maybe earlier too) */ { "DeviceCheck", "DeviceCheck", new Version (11, 0), new Version (13, 0) }, { "IdentityLookup", "IdentityLookup", 11 }, - { "IOSurface", "IOSurface", new Version (11, 0), NotAvailableInSimulator /* Not available in the simulator (the header is there, but broken) */ }, + { "IOSurface", "IOSurface", new Version (11, 0), new Version (26, 0) /* The headers were broken at some point, not sure when they started working again */ }, { "CoreML", "CoreML", 11 }, { "Vision", "Vision", 11 }, { "FileProvider", "FileProvider", 11 }, @@ -554,7 +554,7 @@ public static Frameworks TVOSFrameworks { { "DeviceCheck", "DeviceCheck", new Version (11, 0), new Version (13, 0) }, { "CoreML", "CoreML", 11 }, - { "IOSurface", "IOSurface", new Version (11, 0), NotAvailableInSimulator /* Not available in the simulator (the header is there, but broken) */ }, + { "IOSurface", "IOSurface", new Version (11, 0), new Version (26, 0) /* The headers were broken at some point, not sure when they started working again */ }, { "Vision", "Vision", 11 }, { "CoreServices", "MobileCoreServices", 12 }, diff --git a/tools/common/MachO.cs b/tools/common/MachO.cs index 197c99736be1..85a127cbe821 100644 --- a/tools/common/MachO.cs +++ b/tools/common/MachO.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Text; using Xamarin.Bundler; @@ -90,6 +91,7 @@ public enum LoadCommands : uint { // /* Constants for the cmd field of all load commands, the type */ //#define LC_SEGMENT 0x1 /* segment of this file to be mapped */ //#define LC_SYMTAB 0x2 /* link-edit stab symbol table info */ + Symtab = 0x2, //#define LC_SYMSEG 0x3 /* link-edit gdb symbol table info (obsolete) */ //#define LC_THREAD 0x4 /* thread */ //#define LC_UNIXTHREAD 0x5 /* unix thread (includes a stack) */ @@ -641,6 +643,33 @@ public static bool IsStaticLibrary (string filename, bool throw_if_error = false } } } + + /// + /// Reads a static library or a Mach-O object file and returns the set of unresolved (undefined external) symbols. + /// + public static HashSet GetUnresolvedSymbols (string filename) + { + var symbols = new HashSet (); + using (var fs = File.OpenRead (filename)) + using (var reader = new BinaryReader (fs)) { + if (IsStaticLibrary (reader)) { + var lib = new StaticLibrary (); + lib.Read (filename, reader, fs.Length); + foreach (var obj in lib.ObjectFiles) { + foreach (var sym in obj.GetUnresolvedSymbols (reader)) + symbols.Add (sym); + } + } else if (MachOFile.IsMachOLibrary (null, reader)) { + var obj = new MachOFile (filename); + obj.Read (reader); + foreach (var sym in obj.GetUnresolvedSymbols (reader)) + symbols.Add (sym); + } else { + throw ErrorHelper.CreateError (1601, Errors.MT1601, System.Text.Encoding.ASCII.GetString (reader.ReadBytes (7), 0, 7)); + } + } + return symbols; + } } public class MachOFile { @@ -657,6 +686,7 @@ public class MachOFile { uint _reserved; bool is64bitheader; + long streamBasePosition; // position in the stream where this Mach-O header starts public int cputype { get { return is_big_endian ? MachO.ToBigEndian (_cputype) : _cputype; } } public int cpusubtype { get { return is_big_endian ? MachO.ToBigEndian (_cpusubtype) : _cpusubtype; } } @@ -728,6 +758,8 @@ internal static bool IsMachOLibrary (FatEntry? fat_entry, BinaryReader reader, b internal void Read (BinaryReader reader) { + streamBasePosition = reader.BaseStream.Position; + /* definitions from: /usr/include/mach-o/loader.h */ /* * The 32-bit mach header appears at the very beginning of the object file for @@ -868,6 +900,16 @@ internal void Read (BinaryReader reader) } lc = buildVer; break; + case MachO.LoadCommands.Symtab: + var symtabCmd = new SymtabLoadCommand (); + symtabCmd.cmd = reader.ReadUInt32 (); + symtabCmd.cmdsize = reader.ReadUInt32 (); + symtabCmd.symoff = reader.ReadUInt32 (); + symtabCmd.nsyms = reader.ReadUInt32 (); + symtabCmd.stroff = reader.ReadUInt32 (); + symtabCmd.strsize = reader.ReadUInt32 (); + lc = symtabCmd; + break; default: lc = new LoadCommand (); lc.cmd = reader.ReadUInt32 (); @@ -893,6 +935,58 @@ public bool IsDynamicLibrary { public bool IsObjectFile { get => filetype == MachO.MH_OBJECT; } + + const byte N_EXT = 0x01; // external symbol + const byte N_TYPE = 0x0e; // mask for type bits + const byte N_UNDF = 0x0; // undefined symbol + + /// + /// Reads unresolved (undefined external) symbols from this Mach-O file. + /// The reader must be the same stream used to read this file. + /// + public HashSet GetUnresolvedSymbols (BinaryReader reader) + { + var symbols = new HashSet (); + var symtab = load_commands.OfType ().FirstOrDefault (); + if (symtab is null || symtab.nsyms == 0) + return symbols; + + // Read the string table + reader.BaseStream.Position = streamBasePosition + symtab.stroff; + var stringTable = reader.ReadBytes ((int) symtab.strsize); + + // Read symbol table entries + reader.BaseStream.Position = streamBasePosition + symtab.symoff; + var nlistSize = is64bitheader ? 16 : 12; + for (uint i = 0; i < symtab.nsyms; i++) { + var n_strx = reader.ReadUInt32 (); + var n_type = reader.ReadByte (); + var n_sect = reader.ReadByte (); + var n_desc = reader.ReadInt16 (); + if (is64bitheader) + reader.ReadUInt64 (); // n_value (8 bytes) + else + reader.ReadUInt32 (); // n_value (4 bytes) + + // Filter for undefined external symbols (equivalent of nm -u) + if ((n_type & N_EXT) == 0) + continue; + if ((n_type & N_TYPE) != N_UNDF) + continue; + + // Read symbol name from string table + if (n_strx >= symtab.strsize) + continue; + var end = (int) n_strx; + while (end < stringTable.Length && stringTable [end] != 0) + end++; + var name = Encoding.UTF8.GetString (stringTable, (int) n_strx, end - (int) n_strx); + if (name.Length > 0) + symbols.Add (name); + } + + return symbols; + } } public class FatFile { @@ -1148,4 +1242,11 @@ public MachO.Platform Platform { get { return (MachO.Platform) platform; } } } + + public class SymtabLoadCommand : LoadCommand { + public uint symoff; // offset to symbol table entries + public uint nsyms; // number of symbol table entries + public uint stroff; // offset to string table + public uint strsize; // size of string table in bytes + } } diff --git a/tools/common/create-makefile-fragment.sh b/tools/common/create-makefile-fragment.sh index 5314b052fcf4..2b398007b962 100755 --- a/tools/common/create-makefile-fragment.sh +++ b/tools/common/create-makefile-fragment.sh @@ -40,17 +40,7 @@ if test -z "$FRAGMENT_PATH"; then FRAGMENT_PATH=$PROJECT_FILE.inc fi -if test -z "$DOTNET"; then - echo "The DOTNET environment variable isn't set to the location of the 'dotnet' executable" - exit 1 -fi - -# Our local 'dotnet' executable might not be available yet, in which case don't do anything. -if ! test -f "$DOTNET"; then - exit 0 -fi - -BUILD_EXECUTABLE="$DOTNET build" +BUILD_EXECUTABLE="dotnet build" if test -z "$BUILD_VERBOSITY"; then BUILD_VERBOSITY=/verbosity:diag diff --git a/tools/dotnet-linker/AppBundleRewriter.cs b/tools/dotnet-linker/AppBundleRewriter.cs index 1f003ad9a315..4f1ba83172c4 100644 --- a/tools/dotnet-linker/AppBundleRewriter.cs +++ b/tools/dotnet-linker/AppBundleRewriter.cs @@ -382,6 +382,12 @@ public TypeReference ObjCRuntime_BlockLiteral { } } + public TypeReference ObjCRuntime_Dlfcn { + get { + return GetTypeReference (PlatformAssembly, "ObjCRuntime.Dlfcn", out var _); + } + } + public TypeReference ObjCRuntime_IManagedRegistrar { get { return GetTypeReference (PlatformAssembly, "ObjCRuntime.IManagedRegistrar", out var _); @@ -450,6 +456,12 @@ public MethodReference Nullable_Value { } } + public MethodReference Nullable_ctor { + get { + return GetMethodReference (CorlibAssembly, System_Nullable_1, ".ctor", isStatic: false, System_Nullable_1.GenericParameters [0]); + } + } + public MethodReference Type_GetTypeFromHandle { get { return GetMethodReference (CorlibAssembly, System_Type, "GetTypeFromHandle", isStatic: true, System_RuntimeTypeHandle); diff --git a/tools/dotnet-linker/LinkerConfiguration.cs b/tools/dotnet-linker/LinkerConfiguration.cs index 2082ebb2e36c..0b36112350a3 100644 --- a/tools/dotnet-linker/LinkerConfiguration.cs +++ b/tools/dotnet-linker/LinkerConfiguration.cs @@ -30,6 +30,13 @@ public class LinkerConfiguration { public string IntermediateLinkDir { get; private set; } = string.Empty; public bool InvariantGlobalization { get; private set; } public bool HybridGlobalization { get; private set; } + public InlineDlfcnMethodsMode InlineDlfcnMethods { get; set; } + public bool InlineDlfcnMethodsEnabled => InlineDlfcnMethods != InlineDlfcnMethodsMode.Disabled; + // Per-assembly field symbols collected by InlineDlfcnMethodsStep, keyed by assembly name. + public Dictionary> InlinedDlfcnFields { get; } = new Dictionary> (); + // All [Field] symbol names collected by ProcessExportedFields, used in compatibility mode. + public HashSet FieldSymbols { get; } = new HashSet (); + public string IntermediateOutputPath { get; private set; } = string.Empty; public string ItemsDirectory { get; private set; } = string.Empty; public bool IsSimulatorBuild { get; private set; } public string PartialStaticRegistrarLibrary { get; set; } = string.Empty; @@ -46,6 +53,7 @@ public class LinkerConfiguration { public Application Application { get; private set; } public IList RegistrationMethods { get; set; } = new List (); + public List NativeCodeToCompileAndLink { get; private set; } = new List (); public CompilerFlags CompilerFlags; LinkContext? context; @@ -194,9 +202,22 @@ public static LinkerConfiguration GetInstance (LinkContext context) case "FrameworkAssembly": FrameworkAssemblies.Add (value); break; + case "InlineDlfcnMethods": + if (Enum.TryParse (value, true, out var inlineDlfcnMode)) + InlineDlfcnMethods = inlineDlfcnMode; + else if (string.Equals (value, "compatibility", StringComparison.OrdinalIgnoreCase)) + InlineDlfcnMethods = InlineDlfcnMethodsMode.Compat; + else if (string.IsNullOrEmpty (value)) + InlineDlfcnMethods = InlineDlfcnMethodsMode.Disabled; + else + throw new InvalidOperationException ($"Unknown InlineDlfcnMethods value: {value}"); + break; case "IntermediateLinkDir": IntermediateLinkDir = value; break; + case "IntermediateOutputPath": + IntermediateOutputPath = value; + break; case "Interpreter": if (!string.IsNullOrEmpty (value)) Application.ParseInterpreter (value); @@ -516,7 +537,9 @@ public void Write () foreach (var lib in Application.DylibsToConvertToFrameworks.OrderBy (v => v)) Console.WriteLine ($" {lib}"); Console.WriteLine ($" EnableSGenConc {Application.EnableSGenConc}"); + Console.WriteLine ($" InlineDlfcnMethods: {InlineDlfcnMethods}"); Console.WriteLine ($" IntermediateLinkDir: {IntermediateLinkDir}"); + Console.WriteLine ($" IntermediateOutputPath: {IntermediateOutputPath}"); Console.WriteLine ($" InterpretedAssemblies: {string.Join (", ", Application.InterpretedAssemblies)}"); Console.WriteLine ($" ItemsDirectory: {ItemsDirectory}"); Console.WriteLine ($" {FrameworkAssemblies.Count} framework assemblies:"); @@ -631,3 +654,9 @@ public MSBuildItem (string include, Dictionary metadata) Metadata = metadata; } } + +public enum InlineDlfcnMethodsMode { + Disabled, + Strict, + Compat, +} diff --git a/tools/dotnet-linker/Steps/GenerateInlinedDlfcnNativeCodeStep.cs b/tools/dotnet-linker/Steps/GenerateInlinedDlfcnNativeCodeStep.cs new file mode 100644 index 000000000000..f51105a86b10 --- /dev/null +++ b/tools/dotnet-linker/Steps/GenerateInlinedDlfcnNativeCodeStep.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.IO; + +using Xamarin.Bundler; +using Xamarin.Linker; + +#nullable enable + +namespace Xamarin.Linker.Steps; + +// See docs/code/native-symbols.md for an overview of native symbol handling. +public class GenerateInlinedDlfcnNativeCodeStep : ConfigurationAwareStep { + protected override string Name { get; } = "Generate Inlined Dlfcn Native Code"; + protected override int ErrorCode { get; } = 2360; + + protected override void TryEndProcess () + { + base.TryEndProcess (); + + if (string.IsNullOrEmpty (Configuration.IntermediateOutputPath)) + return; + + // Merge all symbols from all assemblies into a single set to avoid duplicate native symbols. + var allSymbols = new HashSet (); + foreach (var kvp in Configuration.InlinedDlfcnFields) { + foreach (var symbol in kvp.Value) + allSymbols.Add (symbol); + } + + if (allSymbols.Count == 0) + return; + + // Write the symbol list to a file for the PostTrimmingProcessing MSBuild task to consume. + var dir = Path.Combine (Configuration.IntermediateOutputPath, "inlined-dlfcn"); + Directory.CreateDirectory (dir); + var path = Path.Combine (dir, "inlined-dlfcn-symbols.txt"); + var sorted = new List (allSymbols); + sorted.Sort (); + Driver.WriteIfDifferent (path, string.Join ("\n", sorted) + "\n"); + } +} diff --git a/tools/dotnet-linker/Steps/InlineDlfcnMethodsStep.cs b/tools/dotnet-linker/Steps/InlineDlfcnMethodsStep.cs new file mode 100644 index 000000000000..647d39828bc5 --- /dev/null +++ b/tools/dotnet-linker/Steps/InlineDlfcnMethodsStep.cs @@ -0,0 +1,948 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; +using Mono.Linker; +using Mono.Linker.Steps; +using Mono.Tuner; +using MonoTouch.Tuner; + +using Xamarin.Bundler; + +#nullable enable + +namespace Xamarin.Linker.Steps; + +// See docs/code/native-symbols.md for an overview of native symbol handling. +public class InlineDlfcnMethodsStep : ConfigurationAwareMarkHandler { + + protected override string Name { get; } = "Inline Dlfcn Methods"; + protected override int ErrorCode { get; } = 2250; + + AppBundleRewriter abr { get { return Configuration.AppBundleRewriter; } } + + AssemblyDefinition? linkerCurrentAssembly; + bool strictMode; + + public override void Initialize (LinkContext context, MarkContext markContext) + { + base.Initialize (context); + strictMode = Configuration.InlineDlfcnMethods == InlineDlfcnMethodsMode.Strict; + markContext.RegisterMarkMethodAction (Process); + } + + protected override void Process (TypeDefinition type) + { + if (type.HasNestedTypes) { + foreach (var nested in type.NestedTypes) + ProcessType (nested); + } + + if (type.HasMethods) { + foreach (var method in type.Methods) + ProcessMethod (method); + } + } + + TypeDefinition GetDlfcnType (ModuleDefinition module) + { + var dlfcn = module.Types.FirstOrDefault (t => t.Name == "Dlfcn" && t.Namespace == "ObjCRuntime"); + if (dlfcn is null) { + dlfcn = new TypeDefinition ("ObjCRuntime", "Dlfcn", TypeAttributes.Public | TypeAttributes.Sealed, module.TypeSystem.Object); + module.Types.Add (dlfcn); + } + return dlfcn; + } + + void AddField (string assemblyName, string symbolName) + { + if (!Configuration.InlinedDlfcnFields.TryGetValue (assemblyName, out var set)) { + set = new HashSet (); + Configuration.InlinedDlfcnFields [assemblyName] = set; + } + set.Add (symbolName); + } + + MethodDefinition GetOrCreatePInvokeMethod (MethodDefinition callingMethod, string symbolName) + { + var dlfcn = GetDlfcnType (callingMethod.Module); + var methodName = $"xamarin_Dlfcn_{symbolName}_Native"; + var nativeMethod = methodName; + var rv = dlfcn.Methods.FirstOrDefault (m => m.Name == methodName); + if (rv is not null) + return rv; // already exists, no need to create it again + + // [DllImport ("__Internal")] + // static extern IntPtr xamarin_Dlfcn_{symbolName}_Native (); + + rv = new MethodDefinition (methodName, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.PInvokeImpl, abr.System_IntPtr); + rv.IsPreserveSig = true; + + var mod = callingMethod.Module.ModuleReferences.FirstOrDefault (mr => mr.Name == "__Internal"); + if (mod is null) { + mod = new ModuleReference ("__Internal"); + callingMethod.Module.ModuleReferences.Add (mod); + } + rv.PInvokeInfo = new PInvokeInfo (PInvokeAttributes.CharSetNotSpec | PInvokeAttributes.CallConvCdecl, nativeMethod, mod); + + dlfcn.Methods.Add (rv); + Context.Annotations.Mark (rv); + + AddField (callingMethod.Module.Assembly.Name.Name, symbolName); + + return rv; + } + + MethodDefinition GetOrCreateGetSymbolMethod (MethodDefinition callingMethod, string symbolName) + { + var dlfcn = GetDlfcnType (callingMethod.Module); + var methodName = $"Get__{symbolName}"; + var symbolMethod = dlfcn.Methods.FirstOrDefault (m => m.Name == methodName); + if (symbolMethod is not null) + return symbolMethod; // already exists, no need to create it again + + // static bool Get__{symbolName}_Initialized; + // static IntPtr Get__{symbolName}_Cached; + // static IntPtr Get__{symbolName} () + // { + // if (!Get__{symbolName}_Initialized) { + // Get__{symbolName}_Cached = xamarin_Dlfcn_{symbolName}_Native (); + // Get__{symbolName}_Initialized = true; + // } + // return Get__{symbolName}_Cached; + // } + + var initializedField = new FieldDefinition ($"Get__{symbolName}_Initialized", FieldAttributes.Private | FieldAttributes.Static, callingMethod.Module.TypeSystem.Boolean); + dlfcn.Fields.Add (initializedField); + Context.Annotations.Mark (initializedField); + + var cachedField = new FieldDefinition ($"Get__{symbolName}_Cached", FieldAttributes.Private | FieldAttributes.Static, abr.System_IntPtr); + dlfcn.Fields.Add (cachedField); + Context.Annotations.Mark (cachedField); + + var intptr = abr.System_IntPtr; + symbolMethod = new MethodDefinition (methodName, MethodAttributes.Public | MethodAttributes.Static, intptr); + dlfcn.Methods.Add (symbolMethod); + Context.Annotations.Mark (symbolMethod); + + var body = symbolMethod.Body; + var il = body.GetILProcessor (); + + var loadCachedFieldInstruction = il.Create (OpCodes.Ldsfld, cachedField); + + // if (!Get__{symbolName}_Initialized) { + il.Append (il.Create (OpCodes.Ldsfld, initializedField)); + il.Append (il.Create (OpCodes.Brtrue, loadCachedFieldInstruction)); + + // Get__{symbolName}_Cached = xamarin_Dlfcn_{symbolName}_Native (); + il.Append (il.Create (OpCodes.Call, GetOrCreatePInvokeMethod (callingMethod, symbolName))); + il.Append (il.Create (OpCodes.Stsfld, cachedField)); + + // Get__{symbolName}_Initialized = true; + il.Append (il.Create (OpCodes.Ldc_I4_1)); + il.Append (il.Create (OpCodes.Stsfld, initializedField)); + + // return Get__{symbolName}_Cached; + il.Append (loadCachedFieldInstruction); + il.Append (il.Create (OpCodes.Ret)); + + return symbolMethod; + } + + MethodDefinition GetOrCreateGetNativeFieldMethod (MethodDefinition callingMethod, TypeReference fieldType, string symbolName) + { + var dlfcn = GetDlfcnType (callingMethod.Module); + var methodName = $"Get__{symbolName}_{fieldType.Name}"; + var rv = dlfcn.Methods.FirstOrDefault (m => m.Name == methodName); + if (rv is not null) + return rv; // already exists, no need to create it again + + // static FieldType Get__{symbolName}_{fieldType} () + // { + // var ptr = Get__{symbolName} (); + // if (ptr == IntPtr.Zero) + // return default; + // + // /* if value type */ + // return *(FieldType*)ptr; + // + // /* if not value type */ + // return Runtime.GetNSObject (*ptr); + // } + + var importedFieldType = callingMethod.Module.ImportReference (fieldType); + rv = new MethodDefinition (methodName, MethodAttributes.Public | MethodAttributes.Static, importedFieldType); + dlfcn.Methods.Add (rv); + Context.Annotations.Mark (rv); + + var body = rv.Body; + var il = body.GetILProcessor (); + + var ptrVariable = new VariableDefinition (abr.System_IntPtr); + body.Variables.Add (ptrVariable); + + var loadPointerInstructionStart = il.Create (OpCodes.Ldloc, ptrVariable); + + // var ptr = Get__{symbolName} (); + il.Append (il.Create (OpCodes.Call, GetOrCreateGetSymbolMethod (callingMethod, symbolName))); + il.Append (il.Create (OpCodes.Stloc, ptrVariable)); + + // if (ptr == IntPtr.Zero) + il.Append (il.Create (OpCodes.Ldloc, ptrVariable)); + il.Append (il.Create (OpCodes.Ldsfld, abr.System_IntPtr_Zero)); + il.Append (il.Create (OpCodes.Bne_Un, loadPointerInstructionStart)); + + // return default; + var fullFieldTypeName = fieldType.FullName; + switch (fullFieldTypeName) { + case "System.Byte": + case "System.SByte": + case "System.Int16": + case "System.UInt16": + case "System.Int32": + case "System.UInt32": + il.Append (il.Create (OpCodes.Ldc_I4_0)); + break; + case "System.Int64": + il.Append (il.Create (OpCodes.Ldc_I4_0)); + il.Append (il.Create (OpCodes.Conv_I8)); + break; + case "System.UInt64": + il.Append (il.Create (OpCodes.Ldc_I4_0)); + il.Append (il.Create (OpCodes.Conv_U8)); + break; + case "System.Single": + il.Append (il.Create (OpCodes.Ldc_R4, 0f)); + break; + case "System.Double": + il.Append (il.Create (OpCodes.Ldc_R8, 0.0)); + break; + case "System.IntPtr": + case "System.UIntPtr": + il.Append (il.Create (OpCodes.Ldc_I4_0)); + il.Append (il.Create (OpCodes.Conv_I)); + break; + default: + if (fieldType.IsValueType) { + if (fieldType.IsPrimitive) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2254 /* Unsupported primitive field type '{0}' for symbol '{1}' in method '{2}'. Sub-optimal but functional code will be generated. Please file an issue at https://github.com/dotnet/macios/issues/new. */, callingMethod, Errors.MX2254, fieldType.FullName, symbolName, FormatMethod (callingMethod))); + } + var defaultTemporary = new VariableDefinition (importedFieldType); + body.Variables.Add (defaultTemporary); + il.Append (il.Create (OpCodes.Ldloca, defaultTemporary)); + il.Append (il.Create (OpCodes.Initobj, importedFieldType)); + il.Append (il.Create (OpCodes.Ldloc, defaultTemporary)); + } else { + il.Append (il.Create (OpCodes.Ldnull)); + } + break; + } + il.Append (il.Create (OpCodes.Ret)); + + // /* if value type */ + // return *(FieldType*)ptr; + // /* if not value type */ + // return Runtime.GetNSObject (*(IntPtr*)ptr); + il.Append (loadPointerInstructionStart); // il.Create (OpCodes.Ldloc, ptrVariable); + if (fieldType.IsValueType) { + switch (fieldType.FullName) { + case "System.Byte": + il.Append (il.Create (OpCodes.Ldind_U1)); + break; + case "System.SByte": + il.Append (il.Create (OpCodes.Ldind_I1)); + break; + case "System.Int16": + il.Append (il.Create (OpCodes.Ldind_I2)); + break; + case "System.UInt16": + il.Append (il.Create (OpCodes.Ldind_U2)); + break; + case "System.Int32": + il.Append (il.Create (OpCodes.Ldind_I4)); + break; + case "System.UInt32": + il.Append (il.Create (OpCodes.Ldind_U4)); + break; + case "System.Int64": + case "System.UInt64": + il.Append (il.Create (OpCodes.Ldind_I8)); + break; + case "System.Single": + il.Append (il.Create (OpCodes.Ldind_R4)); + break; + case "System.Double": + il.Append (il.Create (OpCodes.Ldind_R8)); + break; + case "System.IntPtr": + case "System.UIntPtr": + il.Append (il.Create (OpCodes.Ldind_I)); + break; + default: + if (fieldType.IsPrimitive) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2254 /* Unsupported primitive field type '{0}' for symbol '{1}' in method '{2}'. Sub-optimal but functional code will be generated. Please file an issue at https://github.com/dotnet/macios/issues/new. */, callingMethod, Errors.MX2254, fieldType.FullName, symbolName, FormatMethod (callingMethod))); + } + il.Append (il.Create (OpCodes.Ldobj, importedFieldType)); + break; + } + } else { + if (!IsNSObjectSubclass (fieldType)) + Report (ErrorHelper.CreateWarning (Configuration.Application, 2256 /* The field type '{0}' for symbol '{1}' in method '{2}' does not appear to be an NSObject subclass. This may cause a runtime failure. */, callingMethod, Errors.MX2256, fieldType.FullName, symbolName, FormatMethod (callingMethod))); + il.Append (il.Create (OpCodes.Ldind_I)); + var getnsobject = abr.Runtime_GetNSObject_T___System_IntPtr.CreateGenericInstanceMethod (importedFieldType); + il.Append (il.Create (OpCodes.Call, getnsobject)); + } + il.Append (il.Create (OpCodes.Ret)); + + return rv; + } + + MethodDefinition GetOrCreateSetNativeFieldMethod (MethodDefinition callingMethod, TypeReference fieldType, string symbolName) + { + var dlfcn = GetDlfcnType (callingMethod.Module); + var methodName = $"Set__{symbolName}_{fieldType.Name}"; + var rv = dlfcn.Methods.FirstOrDefault (m => m.Name == methodName); + if (rv is not null) + return rv; // already exists, no need to create it again + + // static void Set__{symbolName}_{fieldType} ({FieldType} value) + // { + // var ptr = Get__{symbolName} (); + // if (ptr == IntPtr.Zero) + // return; + // + // /* if value type */ + // *(FieldType*)ptr = value; + // + // /* if not value type */ + // *(IntPtr*)ptr = (IntPtr) Runtime.RetainNSObject (value) + // } + // + // Notes: + // * Just like the Dlfcn method(s), this generated code does not release an existing value of a field. + + var importedFieldType = callingMethod.Module.ImportReference (fieldType); + rv = new MethodDefinition (methodName, MethodAttributes.Public | MethodAttributes.Static, abr.System_Void); + rv.Parameters.Add (new ParameterDefinition ("value", ParameterAttributes.None, importedFieldType)); + dlfcn.Methods.Add (rv); + Context.Annotations.Mark (rv); + + var body = rv.Body; + var il = body.GetILProcessor (); + + var ptrVariable = new VariableDefinition (abr.System_IntPtr); + body.Variables.Add (ptrVariable); + + var loadPointerInstructionStart = il.Create (OpCodes.Ldloc, ptrVariable); + + // var ptr = Get__{symbolName} (); + il.Append (il.Create (OpCodes.Call, GetOrCreateGetSymbolMethod (callingMethod, symbolName))); + il.Append (il.Create (OpCodes.Stloc, ptrVariable)); + + // if (ptr == IntPtr.Zero) + il.Append (il.Create (OpCodes.Ldloc, ptrVariable)); + il.Append (il.Create (OpCodes.Ldsfld, abr.System_IntPtr_Zero)); + il.Append (il.Create (OpCodes.Bne_Un, loadPointerInstructionStart)); + // return; + il.Append (il.Create (OpCodes.Ret)); + + // /* if value type */ + // *(FieldType*)ptr = value; + // /* if not value type */ + // *(IntPtr*)ptr = (IntPtr) Runtime.RetainNSObject (value) + il.Append (loadPointerInstructionStart); // il.Create (OpCodes.Ldloc, ptrVariable); + il.Append (il.Create (OpCodes.Ldarg_0)); + if (fieldType.IsValueType) { + switch (fieldType.FullName) { + case "System.Byte": + case "System.SByte": + il.Append (il.Create (OpCodes.Stind_I1)); + break; + case "System.Int16": + case "System.UInt16": + il.Append (il.Create (OpCodes.Stind_I2)); + break; + case "System.Int32": + case "System.UInt32": + il.Append (il.Create (OpCodes.Stind_I4)); + break; + case "System.Int64": + case "System.UInt64": + il.Append (il.Create (OpCodes.Stind_I8)); + break; + case "System.Single": + il.Append (il.Create (OpCodes.Stind_R4)); + break; + case "System.Double": + il.Append (il.Create (OpCodes.Stind_R8)); + break; + case "System.IntPtr": + case "System.UIntPtr": + il.Append (il.Create (OpCodes.Stind_I)); + break; + default: + if (fieldType.IsPrimitive) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2254 /* Unsupported primitive field type '{0}' for symbol '{1}' in method '{2}'. Sub-optimal but functional code will be generated. Please file an issue at https://github.com/dotnet/macios/issues/new. */, callingMethod, Errors.MX2254, fieldType.FullName, symbolName, FormatMethod (callingMethod))); + } + il.Append (il.Create (OpCodes.Stobj, importedFieldType)); + break; + } + } else { + if (!IsNSObjectSubclass (fieldType)) + Report (ErrorHelper.CreateWarning (Configuration.Application, 2256 /* The field type '{0}' for symbol '{1}' in method '{2}' does not appear to be an NSObject subclass. This may cause a runtime failure. */, callingMethod, Errors.MX2256, fieldType.FullName, symbolName, FormatMethod (callingMethod))); + il.Append (il.Create (OpCodes.Call, abr.Runtime_RetainNSObject)); + il.Append (il.Create (OpCodes.Call, abr.NativeObject_op_Implicit_IntPtr)); + il.Append (il.Create (OpCodes.Stind_I)); + } + il.Append (il.Create (OpCodes.Ret)); + + return rv; + } + + MethodDefinition GetOrCreateSetNativeStringMethod (MethodDefinition callingMethod, string symbolName) + { + var dlfcn = GetDlfcnType (callingMethod.Module); + var methodName = $"Set__{symbolName}_String"; + var rv = dlfcn.Methods.FirstOrDefault (m => m.Name == methodName); + if (rv is not null) + return rv; // already exists, no need to create it again + + // static FieldType Set__{symbolName}_String (string? value) + // { + // var ptr = Get__{symbolName} (); + // if (ptr == IntPtr.Zero) + // return; + // + // *(IntPtr*)ptr = (IntPtr) CFString.CreateNative (value); + // } + // + // Notes: + // * Just like the Dlfcn method(s), this generated code does not release an existing value of a field. + + rv = new MethodDefinition (methodName, MethodAttributes.Public | MethodAttributes.Static, abr.System_Void); + rv.Parameters.Add (new ParameterDefinition ("value", ParameterAttributes.None, abr.System_String)); + dlfcn.Methods.Add (rv); + Context.Annotations.Mark (rv); + + var body = rv.Body; + var il = body.GetILProcessor (); + + var ptrVariable = new VariableDefinition (abr.System_IntPtr); + body.Variables.Add (ptrVariable); + + var loadPointerInstructionStart = il.Create (OpCodes.Ldloc, ptrVariable); + + // var ptr = Get__{symbolName} (); + il.Append (il.Create (OpCodes.Call, GetOrCreateGetSymbolMethod (callingMethod, symbolName))); + il.Append (il.Create (OpCodes.Stloc, ptrVariable)); + + // if (ptr == IntPtr.Zero) + il.Append (il.Create (OpCodes.Ldloc, ptrVariable)); + il.Append (il.Create (OpCodes.Ldsfld, abr.System_IntPtr_Zero)); + il.Append (il.Create (OpCodes.Bne_Un, loadPointerInstructionStart)); + // return; + il.Append (il.Create (OpCodes.Ret)); + + // *(IntPtr*)ptr = (IntPtr) CFString.CreateNative (value); + il.Append (loadPointerInstructionStart); // il.Create (OpCodes.Ldloc, ptrVariable); + il.Append (il.Create (OpCodes.Ldarg_0)); + il.Append (il.Create (OpCodes.Call, abr.CFString_CreateNative)); + il.Append (il.Create (OpCodes.Call, abr.NativeObject_op_Implicit_IntPtr)); + il.Append (il.Create (OpCodes.Stind_I)); + il.Append (il.Create (OpCodes.Ret)); + + return rv; + } + + protected override void Process (MethodDefinition method) + { + if (!method.HasBody) + return; + + if (method.DeclaringType.Name == "Dlfcn" && method.DeclaringType.Namespace == "ObjCRuntime") + return; // don't process the Dlfcn methods themselves + + var methodAssembly = method.Module.Assembly; + + if (linkerCurrentAssembly != methodAssembly) { + if (linkerCurrentAssembly is not null) + abr.ClearCurrentAssembly (); + abr.SetCurrentAssembly (methodAssembly); + linkerCurrentAssembly = methodAssembly; + } + + foreach (var instr in method.Body.Instructions) { + if (instr.Operand is not MethodReference mr) + continue; + if (mr.DeclaringType.Name != "Dlfcn" || mr.DeclaringType.Namespace != "ObjCRuntime") + continue; + + // Handle Dlfcn functions of the form (libraryHandle, symbolName) + if (mr.Parameters.Count == 2 && mr.Parameters [0].ParameterType.FullName == "System.IntPtr" && mr.Parameters [1].ParameterType.FullName == "System.String") { + if (instr.Previous.OpCode != OpCodes.Ldstr) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2255 /* Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2255, FormatMethod (mr), FormatMethod (method))); + continue; + } + + // In compatibility mode, only inline symbols from [Field] attributes. + var ldstr = instr.Previous; + if (ldstr.Operand is not string symbolName) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2255 /* Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2255, FormatMethod (mr), FormatMethod (method))); + continue; + } + if (!strictMode && !Configuration.FieldSymbols.Contains (symbolName)) + continue; + + switch (mr.Name) { + // primitive types + case "GetDouble": + case "GetFloat": + case "GetNFloat": + case "GetIntPtr": + case "GetUIntPtr": + case "GetNInt": + case "GetNUInt": + case "GetInt16": + case "GetUInt16": + case "GetInt32": + case "GetUInt32": + case "GetInt64": + case "GetUInt64": + // non-primitive value types + case "GetCGSize": + case "GetCGRect": + // classes + case "GetNSNumber": + case "GetStringConstant": + ldstr.OpCode = OpCodes.Pop; // just pop the library handle/name, we don't need it + ldstr.Operand = null; + + instr.OpCode = OpCodes.Call; + instr.Operand = GetOrCreateGetNativeFieldMethod (method, mr.ReturnType, symbolName); + + abr.SaveCurrentAssembly (); + continue; + case "GetStruct": + if (mr is not GenericInstanceMethod gim || gim.GenericArguments.Count != 1) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2255 /* Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2255, FormatMethod (mr), FormatMethod (method))); + continue; + } + var returnType = gim.GenericArguments [0]; + if (returnType.IsGenericInstance) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2255 /* Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2255, FormatMethod (mr), FormatMethod (method))); + continue; + } + + ldstr.OpCode = OpCodes.Pop; // just pop the library handle/name, we don't need it + ldstr.Operand = null; + + instr.OpCode = OpCodes.Call; + instr.Operand = GetOrCreateGetNativeFieldMethod (method, returnType, symbolName); + + abr.SaveCurrentAssembly (); + continue; + case "GetIndirect": + case "dlsym": + ldstr.OpCode = OpCodes.Pop; // just pop the library handle/name, we don't need it + ldstr.Operand = null; + + instr.OpCode = OpCodes.Call; + instr.Operand = GetOrCreateGetSymbolMethod (method, symbolName); + + abr.SaveCurrentAssembly (); + continue; + } + } + + // Handle Dlfcn functions of the form (RTLD, symbolName) + if (mr.Parameters.Count == 2 && mr.Parameters [0].ParameterType.FullName == "ObjCRuntime.Dlfcn/RTLD" && mr.Parameters [1].ParameterType.FullName == "System.String") { + if (instr.Previous.OpCode != OpCodes.Ldstr) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2255 /* Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2255, FormatMethod (mr), FormatMethod (method))); + continue; + } + var ldstr = instr.Previous; + if (ldstr.Operand is not string symbolName) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2255 /* Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2255, FormatMethod (mr), FormatMethod (method))); + continue; + } + + // In compatibility mode, only inline symbols from [Field] attributes. + if (!strictMode && !Configuration.FieldSymbols.Contains (symbolName)) + continue; + + + switch (mr.Name) { + case "dlsym": + ldstr.OpCode = OpCodes.Pop; // just pop the library handle/name, we don't need it + ldstr.Operand = null; + + instr.OpCode = OpCodes.Call; + instr.Operand = GetOrCreateGetSymbolMethod (method, symbolName); + + abr.SaveCurrentAssembly (); + continue; + } + } + + // Handle Dlfcn functions of the form (libraryName, symbolName) + if (mr.Parameters.Count == 2 && mr.Parameters [0].ParameterType.FullName == "System.String" && mr.Parameters [1].ParameterType.FullName == "System.String") { + if (instr.Previous.OpCode != OpCodes.Ldstr) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2255 /* Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2255, FormatMethod (mr), FormatMethod (method))); + continue; + } + var ldstr = instr.Previous; + if (ldstr.Operand is not string symbolName) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2255 /* Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2255, FormatMethod (mr), FormatMethod (method))); + continue; + } + + // In compatibility mode, only inline symbols from [Field] attributes. + if (!strictMode && !Configuration.FieldSymbols.Contains (symbolName)) + continue; + + switch (mr.Name) { + // primitive types + case "SlowGetDouble": + case "SlowGetIntPtr": + case "SlowGetInt32": + case "SlowGetInt64": + // classes + case "SlowGetStringConstant": + ldstr.OpCode = OpCodes.Pop; // just pop the library handle/name, we don't need it + ldstr.Operand = null; + + instr.OpCode = OpCodes.Call; + instr.Operand = GetOrCreateGetNativeFieldMethod (method, mr.ReturnType, symbolName); + + abr.SaveCurrentAssembly (); + continue; + } + } + + // Handle Dlfcn functions of the form void (libraryHandle|libraryName, symbolName, value) + if (mr.Parameters.Count == 3 && + (mr.Parameters [0].ParameterType.FullName == "System.String" || mr.Parameters [0].ParameterType.FullName == "System.IntPtr") && + mr.Parameters [1].ParameterType.FullName == "System.String") { + + var ins = instr; + Instruction? ldstr = null; + + // skip any call instructions that take a single argument and return a value, as those are likely to be calls to op_Implicit or op_Explicit functions. + while (ins.Previous.OpCode == OpCodes.Call && ins.Previous.Operand is MethodReference prevMr && !prevMr.ReturnType.Is ("System", "Void") && prevMr.HasParameters && prevMr.Parameters.Count == 1) { + ins = ins.Previous; + } + + switch (ins.Previous.OpCode.StackBehaviourPop) { + case StackBehaviour.Pop0: + switch (ins.Previous.OpCode.StackBehaviourPush) { + case StackBehaviour.Push1: + case StackBehaviour.Pushi: + case StackBehaviour.Pushi8: + case StackBehaviour.Pushr4: + case StackBehaviour.Pushr8: + case StackBehaviour.Pushref: + ldstr = ins.Previous.Previous; + break; + } + break; + case StackBehaviour.Pop1: + case StackBehaviour.Popref: + switch (instr.Previous.OpCode.StackBehaviourPush) { + case StackBehaviour.Push1: + case StackBehaviour.Pushi: + case StackBehaviour.Pushi8: + case StackBehaviour.Pushr4: + case StackBehaviour.Pushr8: + case StackBehaviour.Pushref: + ldstr = instr.Previous.Previous.Previous; + break; + } + break; + } + if (ldstr is null) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2255, method, "Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. Unknown instruction sequence: {2} ({3}/{4}). The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new.", FormatMethod (mr), FormatMethod (method), instr.Previous, instr.Previous.OpCode.StackBehaviourPop, instr.Previous.OpCode.StackBehaviourPush)); + continue; + } + + if (ldstr.OpCode != OpCodes.Ldstr) { + // Report (ErrorHelper.CreateWarning (Configuration.Application, 2255 /* Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2255, FormatMethod (mr), FormatMethod (method))); + Report (ErrorHelper.CreateWarning (Configuration.Application, 2255, method, "Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. Expected 'ldstr' opcode, got '{2}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new.", FormatMethod (mr), FormatMethod (method), ldstr)); + continue; + } + if (ldstr.Operand is not string symbolName) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2255 /* Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2255, FormatMethod (mr), FormatMethod (method))); + continue; + } + + switch (mr.Name) { + // primitive types + case "SetSByte": + case "SetByte": + case "SetInt16": + case "SetUInt16": + case "SetInt32": + case "SetUInt32": + case "SetInt64": + case "SetUInt64": + case "SetArray": + case "SetObject": + case "SetNInt": + case "SetNUInt": + case "SetNFloat": + case "SetUIntPtr": + case "SetIntPtr": + case "SetCGSize": + case "SetDouble": + case "SetFloat": + ldstr.OpCode = OpCodes.Pop; // just pop the library handle/name, we don't need it + ldstr.Operand = null; + + instr.OpCode = OpCodes.Call; + instr.Operand = GetOrCreateSetNativeFieldMethod (method, mr.Parameters [2].ParameterType, symbolName); + + abr.SaveCurrentAssembly (); + continue; + // classes + case "SetString": + if (mr.Parameters [2].ParameterType.FullName == "System.String") { + ldstr.OpCode = OpCodes.Pop; // just pop the library handle/name, we don't need it + ldstr.Operand = null; + + instr.OpCode = OpCodes.Call; + instr.Operand = GetOrCreateSetNativeStringMethod (method, symbolName); + abr.SaveCurrentAssembly (); + continue; + } else if (mr.Parameters [2].ParameterType.FullName == "Foundation.NSString") { + ldstr.OpCode = OpCodes.Pop; // just pop the library handle/name, we don't need it + ldstr.Operand = null; + + instr.OpCode = OpCodes.Call; + instr.Operand = GetOrCreateSetNativeFieldMethod (method, mr.Parameters [2].ParameterType, symbolName); + abr.SaveCurrentAssembly (); + continue; + } + + Report (ErrorHelper.CreateWarning (Configuration.Application, 2255 /* Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2255, FormatMethod (mr), FormatMethod (method))); + continue; + case "CachePointer": + if (!(mr.Parameters [2].ParameterType is PointerType pt && pt.ElementType.FullName == "System.IntPtr")) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2255 /* Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2255, FormatMethod (mr), FormatMethod (method))); + continue; + } + + // + // we're going to replace the entire method body with something like: + // + // var ptr = Get__{symbolName} (); + // if (ptr == IntPtr.Zero) + // return IntPtr.Zero; + // return *(IntPtr *) ptr; + // + + if (!IsGeneratedCachePointerMethod (method, out var cachePointerSymbolName, out var failureMessage)) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2257 /* Unknown IL sequence for method with call to Dlfcn.CachePointer: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2257, failureMessage, FormatMethod (method))); + continue; + } + + if (cachePointerSymbolName != symbolName) { + Report (ErrorHelper.CreateWarning (Configuration.Application, 2257 /* Unknown IL sequence for method with call to Dlfcn.CachePointer: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2257, $"Could not determine symbol name", FormatMethod (method))); + continue; + } + + ldstr.OpCode = OpCodes.Pop; // just pop the library handle/name, we don't need it + ldstr.Operand = null; + + method.Body.Instructions.Clear (); + var il = method.Body.GetILProcessor (); + var ptrVariable = new VariableDefinition (abr.System_IntPtr); + method.Body.Variables.Add (ptrVariable); + var loadPointerInstructionStart = il.Create (OpCodes.Ldloc, ptrVariable); + // var ptr = Get__{symbolName} () + il.Append (il.Create (OpCodes.Call, GetOrCreateGetSymbolMethod (method, symbolName))); + il.Append (il.Create (OpCodes.Stloc, ptrVariable)); + // if (ptr == IntPtr.Zero) + il.Append (il.Create (OpCodes.Ldloc, ptrVariable)); + il.Append (il.Create (OpCodes.Brtrue_S, loadPointerInstructionStart)); + // return IntPtr.Zero; + il.Append (il.Create (OpCodes.Ldc_I4_0)); + il.Append (il.Create (OpCodes.Conv_I)); + il.Append (il.Create (OpCodes.Ret)); + // return *(IntPtr *) ptr; + il.Append (loadPointerInstructionStart); // il.Create (OpCodes.Ldloc, ptrVariable) + il.Append (il.Create (OpCodes.Ldind_I)); + il.Append (il.Create (OpCodes.Ret)); + + abr.SaveCurrentAssembly (); + return; // we replace the whole method body, so no need to continue processing the method + } + } + + switch (mr.Name) { + case "_dlopen": // nothing to inline here + case "dlopen": // nothing to inline here + case "dlerror": // nothing to inline here + continue; + case "dlclose": + // It might be possible to just remove these calls, because + // (PENDING CONFIRMATION) I believe dlclose is a no-op on at least some Apple platforms. + continue; + default: + Report (ErrorHelper.CreateWarning (Configuration.Application, 2255 /* Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new. */, method, Errors.MX2255, FormatMethod (mr), FormatMethod (method))); + continue; + } + } + } + + static bool IsGeneratedCachePointerMethod (MethodDefinition method, [NotNullWhen (true)] out string? fieldName, [NotNullWhen (false)] out string? failureMessage) + { + fieldName = null; + failureMessage = null; + + // The following code: + // + // get { + // fixed (IntPtr *storage = &values [8]) + // return Dlfcn.CachePointer (Libraries.XYZ.Handle, "...", storage); + // } + // + // has the following IL sequence: + // + // IL_0000: ldsfld System.IntPtr[] ::values + // IL_0005: ldc.i4.0 + // IL_0006: ldelema System.IntPtr + // IL_000b: stloc.1 + // IL_000c: ldloc.1 + // IL_000d: conv.u + // IL_000e: stloc.0 + // IL_000f: ldsfld System.IntPtr ObjCRuntime.Libraries/::Handle + // IL_0014: ldstr "FIELDNAME" + // IL_0019: ldloc.0 + // IL_001a: call System.IntPtr ObjCRuntime.Dlfcn::CachePointer(System.IntPtr,System.String,System.IntPtr*) + // IL_0020: stloc.2 + // IL_0021: br.s IL_0023 + // IL_0023: ldloc.2 + // IL_001f: ret + // + // (the indented code can happen for debug builds) + // + if (!method.HasBody) { + failureMessage = "Method has no body"; + return false; + } + var body = method.Body; + if (body.Instructions.Count == 0) { + failureMessage = "Method has no instructions"; + return false; + } + + var instr = body.Instructions.First (); + var isLast = false; + + bool AssertOpCode ([NotNullWhen (false)] out string? failureMessage, params OpCode [] expected) + { + failureMessage = null; + + while (instr.OpCode == OpCodes.Nop && instr.Next is not null) + instr = instr.Next; + + if (!expected.Any (v => v == instr.OpCode)) { + failureMessage = $"Expected any of '{string.Join (", ", expected.Select (v => v.ToString ()))}' as instruction at offset IL{instr.Offset:X4}, got: {instr}"; + return false; + } + + if (instr.Next is null) { + if (isLast) + return true; + failureMessage = $"Expected more instructions after {instr}."; + return false; + } + + if (isLast) { + failureMessage = $"Got more instructions than expected after {instr}."; + return false; + } + + instr = instr.Next; + + return true; + } + + if (!AssertOpCode (out failureMessage, OpCodes.Ldsfld)) + return false; + + var ldcOpcodes = new OpCode [] { OpCodes.Ldc_I4_0, OpCodes.Ldc_I4_1, OpCodes.Ldc_I4_2, OpCodes.Ldc_I4_3, OpCodes.Ldc_I4_4, OpCodes.Ldc_I4_5, OpCodes.Ldc_I4_6, OpCodes.Ldc_I4_7, OpCodes.Ldc_I4_8, OpCodes.Ldc_I4, OpCodes.Ldc_I4_S }; + if (!AssertOpCode (out failureMessage, ldcOpcodes)) + return false; + + if (!AssertOpCode (out failureMessage, OpCodes.Ldelema)) + return false; + + var stlocOpcodes = new OpCode [] { OpCodes.Stloc_0, OpCodes.Stloc_1, OpCodes.Stloc_2, OpCodes.Stloc_3, OpCodes.Stloc, OpCodes.Stloc_S }; + if (!AssertOpCode (out failureMessage, stlocOpcodes)) + return false; + + var ldlocOpcodes = new OpCode [] { OpCodes.Ldloc_0, OpCodes.Ldloc_1, OpCodes.Ldloc_2, OpCodes.Ldloc_3, OpCodes.Ldloc, OpCodes.Ldloc_S }; + if (!AssertOpCode (out failureMessage, ldlocOpcodes)) + return false; + + if (!AssertOpCode (out failureMessage, OpCodes.Conv_U)) + return false; + + if (!AssertOpCode (out failureMessage, stlocOpcodes)) + return false; + + if (!AssertOpCode (out failureMessage, OpCodes.Ldsfld)) + return false; + + if (!AssertOpCode (out failureMessage, OpCodes.Ldstr)) + return false; + fieldName = (string) instr.Previous.Operand; + + if (!AssertOpCode (out failureMessage, ldlocOpcodes)) + return false; + + if (!AssertOpCode (out failureMessage, OpCodes.Call)) + return false; + + if (stlocOpcodes.Any (v => v == instr.OpCode)) { + if (!AssertOpCode (out failureMessage, stlocOpcodes)) + return false; + + var branchOpcodes = new OpCode [] { OpCodes.Br, OpCodes.Br_S }; + if (!AssertOpCode (out failureMessage, branchOpcodes)) + return false; + + if (!AssertOpCode (out failureMessage, ldlocOpcodes)) + return false; + } + + isLast = true; + return AssertOpCode (out failureMessage, OpCodes.Ret); + } + + static string FormatMethod (MethodReference method) + { + var rv = method.FullName; + var idx = rv.IndexOf (' '); + if (idx > 0) + rv = rv.Substring (idx + 1); + return rv; + } + + static bool IsNSObjectSubclass (TypeReference type) + { + var resolved = type.Resolve (); + while (resolved is not null) { + if (resolved.FullName == "Foundation.NSObject") + return true; + if (resolved.BaseType is null) + break; + resolved = resolved.BaseType.Resolve (); + } + return false; + } +} diff --git a/tools/linker/MonoTouch.Tuner/ListExportedSymbols.cs b/tools/linker/MonoTouch.Tuner/ListExportedSymbols.cs index e014c12bc41c..11af9866a674 100644 --- a/tools/linker/MonoTouch.Tuner/ListExportedSymbols.cs +++ b/tools/linker/MonoTouch.Tuner/ListExportedSymbols.cs @@ -211,7 +211,7 @@ bool ProcessMethod (MethodDefinition method) } } - if (method.IsPropertyMethod ()) { + if (method.IsPropertyMethod () && !Configuration.InlineDlfcnMethodsEnabled) { var property = method.GetProperty (); // The Field attribute may have been linked away, but we've stored it in an annotation. if (property is not null && Annotations.GetCustomAnnotations ("ExportedFields").TryGetValue (property, out var symbol) && symbol is string symbolStr) { diff --git a/tools/linker/MonoTouch.Tuner/ProcessExportedFields.cs b/tools/linker/MonoTouch.Tuner/ProcessExportedFields.cs index 8e4a61ed2578..53787784589f 100644 --- a/tools/linker/MonoTouch.Tuner/ProcessExportedFields.cs +++ b/tools/linker/MonoTouch.Tuner/ProcessExportedFields.cs @@ -23,6 +23,8 @@ namespace MonoTouch.Tuner { // Then at the end of the linker process (ListExportedSymbols step) // we lookup that annotation. // + // See docs/code/native-symbols.md for an overview of native symbol handling. + // public class ProcessExportedFields : BaseStep { protected override void ProcessAssembly (AssemblyDefinition assembly) @@ -55,6 +57,15 @@ void ProcessProperty (PropertyDefinition property) if (!property.HasCustomAttributes) return; + var config = LinkerConfiguration.GetInstance (Context); + + // Collect all [Field] symbol names for InlineDlfcnMethodsStep's compatibility mode. + if (config.InlineDlfcnMethodsEnabled) { + var allSymbol = GetFieldSymbolName (property); + if (allSymbol is not null) + config.FieldSymbols.Add (allSymbol); + } + var symbol = GetFieldSymbol (property); if (symbol is null) return; @@ -62,6 +73,28 @@ void ProcessProperty (PropertyDefinition property) Annotations.GetCustomAnnotations ("ExportedFields").Add (property, symbol); } + // Returns the symbol name from a [Field] attribute, regardless of library. + internal static string? GetFieldSymbolName (PropertyDefinition property) + { + if (!property.HasCustomAttributes) + return null; + + foreach (var attrib in property.CustomAttributes) { + var declaringType = attrib.Constructor.DeclaringType.Resolve (); + + if (!declaringType.Is (Namespaces.Foundation, "FieldAttribute")) + continue; + + if (attrib.ConstructorArguments.Count < 1) + continue; + + return (string) attrib.ConstructorArguments [0].Value; + } + + return null; + } + + // Returns the symbol name only for __Internal fields. internal static string? GetFieldSymbol (PropertyDefinition property) { if (!property.HasCustomAttributes) diff --git a/tools/mtouch/Errors.designer.cs b/tools/mtouch/Errors.designer.cs index 588591370685..e231c37fb8b1 100644 --- a/tools/mtouch/Errors.designer.cs +++ b/tools/mtouch/Errors.designer.cs @@ -3515,6 +3515,42 @@ public static string MX2112_B { } } + /// + /// Looks up a localized string similar to Unsupported primitive field type '{0}' for symbol '{1}' in method '{2}'. Sub-optimal but functional code will be generated. Please file an issue at https://github.com/dotnet/macios/issues/new.. + /// + public static string MX2254 { + get { + return ResourceManager.GetString("MX2254", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new.. + /// + public static string MX2255 { + get { + return ResourceManager.GetString("MX2255", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The field type '{0}' for symbol '{1}' in method '{2}' does not appear to be an NSObject subclass. This may cause a runtime failure.. + /// + public static string MX2256 { + get { + return ResourceManager.GetString("MX2256", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown IL sequence for method with call to Dlfcn.CachePointer: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new.. + /// + public static string MX2257 { + get { + return ResourceManager.GetString("MX2257", resourceCulture); + } + } + /// /// Looks up a localized string similar to Could not {0} the assembly '{1}'. /// diff --git a/tools/mtouch/Errors.resx b/tools/mtouch/Errors.resx index 9cadfefe00d1..e8381bb78676 100644 --- a/tools/mtouch/Errors.resx +++ b/tools/mtouch/Errors.resx @@ -1092,7 +1092,25 @@ - + + + + + Unsupported primitive field type '{0}' for symbol '{1}' in method '{2}'. Sub-optimal but functional code will be generated. Please file an issue at https://github.com/dotnet/macios/issues/new + + + + Unknown or unsupported Dlfcn pattern: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new + + + + The field type '{0}' for symbol '{1}' in method '{2}' does not appear to be an NSObject subclass. This may cause a runtime failure + + + + Unknown IL sequence for method with call to Dlfcn.CachePointer: '{0}' in method '{1}'. The call will not be inlined. Please file an issue at https://github.com/dotnet/macios/issues/new + + diff --git a/tools/mtouch/Makefile b/tools/mtouch/Makefile index 703caf79aa36..40fab8c5dd39 100644 --- a/tools/mtouch/Makefile +++ b/tools/mtouch/Makefile @@ -20,7 +20,6 @@ DOTNET_PLATFORMS_CORECLR_MTOUCH=$(DOTNET_CORECLR_PLATFORMS) # mtouch.csproj.inc contains the mtouch_dependencies variable used to determine if mtouch needs to be rebuilt or not. mtouch.csproj.inc: export BUILD_VERBOSITY=$(MSBUILD_VERBOSITY) -mtouch.csproj.inc: export DOTNET:=$(DOTNET) -include mtouch.csproj.inc $(LOCAL_MTOUCH): $(mtouch_dependencies) diff --git a/tools/sharpie/Makefile b/tools/sharpie/Makefile index 55232bb37b64..56e53de56372 100644 --- a/tools/sharpie/Makefile +++ b/tools/sharpie/Makefile @@ -8,7 +8,6 @@ SHARPIE_VERSION=$(XCODE_VERSION).$(SHARPIE_VERSION_BUILD).$(XCODE_BUMP_COMMIT_DI # Sharpie.Bind.csproj.inc contains the $(Sharpie.Bind_dependencies) variable used to determine if Sharpie.Bind needs to be rebuilt or not. Sharpie.Bind/Sharpie.Bind.csproj.inc: export BUILD_VERBOSITY=$(MSBUILD_VERBOSITY) -Sharpie.Bind/Sharpie.Bind.csproj.inc: export DOTNET:=$(DOTNET) -include Sharpie.Bind/Sharpie.Bind.csproj.inc SHARPIE_BIND_TOOL_DEBUG=Sharpie.Bind.Tool/bin/Debug/osx-arm64/Sharpie.Bind.Tool diff --git a/tools/sharpie/Sharpie.Bind/Massagers/DeepSplitMassager.cs b/tools/sharpie/Sharpie.Bind/Massagers/DeepSplitMassager.cs new file mode 100644 index 000000000000..f53bceabed33 --- /dev/null +++ b/tools/sharpie/Sharpie.Bind/Massagers/DeepSplitMassager.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using ICSharpCode.NRefactory.CSharp; + +namespace Sharpie.Bind.Massagers; + +/// +/// Splits the generated binding into one .cs file per source header file. +/// Struct/enum declarations go into a separate StructsAndEnums.cs file. +/// +[RegisterBefore (typeof (GenerateUsingStatementsMassager))] +public sealed class DeepSplitMassager : Massager { + readonly Dictionary documentsByHeader = new (StringComparer.OrdinalIgnoreCase); + readonly DocumentSyntaxTree structsAndEnums = new DocumentSyntaxTree ("StructsAndEnums.cs"); + + public DeepSplitMassager (ObjectiveCBinder binder) + : base (binder) + { + } + + DocumentSyntaxTree GetOrCreateDocument (string headerFileName) + { + var baseName = Path.GetFileNameWithoutExtension (headerFileName); + var key = baseName.ToLowerInvariant (); + + if (!documentsByHeader.TryGetValue (key, out var doc)) { + doc = new DocumentSyntaxTree (baseName + ".cs"); + documentsByHeader [key] = doc; + } + return doc; + } + + string? GetSourceHeaderName (AstNode node) + { + // Walk annotations to find the linked Clang declaration and its source location + foreach (var annotation in node.Annotations) { + if (annotation is Cursor cursor) { + if (cursor.TryGetPresumedLoc (out var loc) && loc.HasValue && !string.IsNullOrEmpty (loc.Value.FileName)) + return Path.GetFileName (loc.Value.FileName); + } + } + return null; + } + + public override void VisitTypeDeclaration (TypeDeclaration typeDeclaration) + { + if (HasVisited (typeDeclaration)) + return; + + MarkVisited (typeDeclaration); + typeDeclaration.Remove (); + + if (typeDeclaration.ClassType == ClassType.Interface) { + var headerName = GetSourceHeaderName (typeDeclaration); + if (headerName is not null) { + GetOrCreateDocument (headerName).Members.Add (typeDeclaration); + } else { + GetOrCreateDocument ("ApiDefinition").Members.Add (typeDeclaration); + } + } else { + structsAndEnums.Members.Add (typeDeclaration); + } + } + + public override void VisitDelegateDeclaration (DelegateDeclaration delegateDeclaration) + { + if (HasVisited (delegateDeclaration)) + return; + + MarkVisited (delegateDeclaration); + delegateDeclaration.Remove (); + + var headerName = GetSourceHeaderName (delegateDeclaration); + if (headerName is not null) { + GetOrCreateDocument (headerName).Members.Add (delegateDeclaration); + } else { + GetOrCreateDocument ("ApiDefinition").Members.Add (delegateDeclaration); + } + } + + public override void VisitSyntaxTree (SyntaxTree syntaxTree) + { + base.VisitSyntaxTree (syntaxTree); + + if (syntaxTree.Members.Count > 0) { + Console.Error.WriteLine (syntaxTree); + throw new Exception ("original SyntaxTree should be empty"); + } + + // Add documents sorted by filename for deterministic output + foreach (var doc in documentsByHeader.OrderBy (kv => kv.Key)) { + if (doc.Value.Members.Count > 0) + syntaxTree.Members.Add (doc.Value); + } + + if (structsAndEnums.Members.Count > 0) + syntaxTree.Members.Add (structsAndEnums); + } +} diff --git a/tools/sharpie/Sharpie.Bind/Massagers/PlatformTypeMappingMassager.cs b/tools/sharpie/Sharpie.Bind/Massagers/PlatformTypeMappingMassager.cs index 4a357a14f14c..6137a057916b 100644 --- a/tools/sharpie/Sharpie.Bind/Massagers/PlatformTypeMappingMassager.cs +++ b/tools/sharpie/Sharpie.Bind/Massagers/PlatformTypeMappingMassager.cs @@ -12,7 +12,7 @@ namespace Sharpie.Bind.Massagers; [RegisterBefore (typeof (GenerateUsingStatementsMassager))] public sealed class PlatformTypeMappingMassager : Massager { readonly Dictionary typeMap = new (); - readonly Dictionary protocolMap = new (); + readonly HashSet protocolEntries = new (); readonly Stack ignoreType = new Stack (); public PlatformTypeMappingMassager (ObjectiveCBinder binder) @@ -23,6 +23,7 @@ public PlatformTypeMappingMassager (ObjectiveCBinder binder) public override bool Initialize () { typeMap.Clear (); + protocolEntries.Clear (); var path = base.Binder.PlatformAssembly; var decoder = new TypelessDecoder (); @@ -73,12 +74,30 @@ public override bool Initialize () var etName = mr.GetString (et.Name); nativeName ??= etName; - var map = typeMap; - if (map.Remove (nativeName)) { - // there would be a collision, so skip adding again - continue; + var entry = (etNamespace + "." + etName, etNamespace, etName); + if (typeMap.ContainsKey (nativeName)) { + // When two types map to the same native name, prefer the + // standard protocol interface (named "I" + nativeName, e.g. + // INSCopying for "NSCopying") over a [Model] class or a + // non-standard protocol. If neither or both follow the + // convention, drop both (genuine ambiguity). + bool newIsStandardProtocol = isProtocolAttribute && etName == "I" + nativeName; + bool existingIsStandardProtocol = protocolEntries.Contains (nativeName); + if (newIsStandardProtocol && !existingIsStandardProtocol) { + typeMap [nativeName] = entry; + protocolEntries.Add (nativeName); + } else if (!newIsStandardProtocol && existingIsStandardProtocol) { + // existing is the standard protocol, keep it + } else { + // genuine collision, drop both + typeMap.Remove (nativeName); + protocolEntries.Remove (nativeName); + } + } else { + typeMap.Add (nativeName, entry); + if (isProtocolAttribute && etName == "I" + nativeName) + protocolEntries.Add (nativeName); } - map.Add (nativeName, (etNamespace + "." + etName, etNamespace, etName)); } return typeMap.Count > 0; @@ -207,11 +226,15 @@ public override void VisitTypeOfExpression (TypeOfExpression typeOfExpression) public override void VisitMemberType (MemberType memberType) { + // Visit children first so that type arguments (e.g. inside Action) + // are mapped before the parent type is processed. + base.VisitMemberType (memberType); VisitType (memberType, memberType.MemberName); } public override void VisitSimpleType (SimpleType simpleType) { + base.VisitSimpleType (simpleType); VisitType (simpleType, simpleType.Identifier); } diff --git a/tools/sharpie/Sharpie.Bind/ObjectiveCBinder.cs b/tools/sharpie/Sharpie.Bind/ObjectiveCBinder.cs index 7e76452e4acd..01f5ef80dacd 100644 --- a/tools/sharpie/Sharpie.Bind/ObjectiveCBinder.cs +++ b/tools/sharpie/Sharpie.Bind/ObjectiveCBinder.cs @@ -34,6 +34,7 @@ public string Sdk { public List ClangArguments = new List (); public bool? EnableModules { get; set; } public bool SplitDocuments { get; set; } = true; + public bool DeepSplit { get; set; } public string ApiDefinitionName { get; set; } = "ApiDefinition.cs"; public string StructsAndEnumsName { get; set; } = "StructsAndEnums.cs"; @@ -249,6 +250,10 @@ protected virtual bool AddArguments (List args) args.Add ("--nosplit"); } + if (DeepSplit) { + args.Add ("--deepsplit"); + } + if (EnableModules.HasValue) { if (EnableModules.Value) args.Add ("--modules=true"); @@ -468,7 +473,9 @@ BindingResult BindImpl () var massagerNs = new NamespaceMassager (this, Namespace); massager.RegisterMassager (massagerNs); } - if (SplitDocuments) + if (DeepSplit) + massager.RegisterMassager (new DeepSplitMassager (this)); + else if (SplitDocuments) massager.RegisterMassager (new SyntaxTreeSplitterMassager (this)); foreach (var m in Massagers) { if (m.Enable) { diff --git a/tools/sharpie/Sharpie.Bind/Tools.cs b/tools/sharpie/Sharpie.Bind/Tools.cs index 3381d62a0962..fbc0bc23b027 100644 --- a/tools/sharpie/Sharpie.Bind/Tools.cs +++ b/tools/sharpie/Sharpie.Bind/Tools.cs @@ -44,6 +44,7 @@ public static int Bind (string [] arguments) { "n|namespace=", "Namespace under which to generate the binding. The default is to use the framework's name as the namespace.", v => binder.Namespace = v }, { "m|massage=", "Register (+ prefix) or exclude (- prefix) a massager by name.", v => binder.AddMassager (v) }, { "nosplit", "Do not split the generated binding into multiple files.", v => binder.SplitDocuments = false }, + { "deepsplit", "Split the generated binding into one file per source header.", v => binder.DeepSplit = true }, }; os.EndOfParsingArguments.Clear ();