-
Notifications
You must be signed in to change notification settings - Fork 786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Type provider design-time DLLs should be chosen more appropriately #3736
Comments
In (C#) Roslyn-Analyzer world is it really the compiler figuring out the paths? |
No, it's just a related example
I think its inevitable in some form. The referenced runtime DLL and the design-time DLL live in the same package and we really have to separate them, but equally be able to locate the latter from the former.
The design-time DLL is loaded using LoadFrom and must carry all its dependencies in the same directory as the DLL (or loadable using standard binding rules after a LoadFrom). It must be a standalone "reflection-loaded adding". One huge advantage of the above is that each design-time configuration (e.g. x86 and x64) can now carry different dependencies. Note the situation today is horrendous - the dependencies of the design-time components all end up in the "lib" directory, and mistakenly get added as references. And people hack the search through neighouring packages in terrible ways, e.g. https://github.com/BlueMountainCapital/FSharpRProvider/blob/master/src/RProvider/RProvider.fsx |
Note the other alternative would be to have logic in our MSBuild targets file and add a first-class However I think that's very obscure and won't work for tools like F# Interactive (fsi.exe), which must locate type provider design-time components without resorting to using MSBuild. In particular I don't really want people to have to use
in their F# scripts - though admittedly it's being very honest about what's going on. We would end up baking just the same logic in to the compiler in order to give people a good error message telling them where to find the design-time DLL of they forget to add the |
To be honest that sounds a lot better and if we add a corresponding
Yes but a compiler solution introduces a lot of other problems
On the other side thinking about supporting design time and runtime dependencies in Paket makes my head burn. (In practice a unified resolution will most likely be fine / we might add special supports in our groups). Also there is a problem with detecting those dependencies: Is a dependency of a type provider package a runtime or a designtime dependency? But at least that will not be a problem of the compiler. |
And it will be solved via #2483 ;) |
I see it not as NuGet implementation, but as providing a stable layout relative to the runtime assembly.
This could be done leveraging #2483: #r "typeprovider:path/to/FSharp.Data.dll" paket in the load script generation would identify assemblies exposing type providers and add the reference in that way rather than simple assembly reference. The
Lots of valid concerns. |
I don't expect Paket or Nuget will ever seriously get into the business of managing dependencies of compile-time tools and add-ins. At least not in a timeframe that's useful. Indeed I don't think they could - they just don't know enough about host tooling. We could maybe push something bespoke into Paket, but its only use cases would be for F# type-provider add-ins, so there's not much point. I'm also not terribly keen in hiding resolution logic in MSBuild for similar reasons. On the whole I'd rather have this stuff "close to home" where we have control over it, rather than pushing the problem off to Paket, Nuget etc. If it inspires Nuget to come up with a way of managing compile-time tool dependency chains then that's good and we could eventually switch to using the results, but I think that's unlikely. Even for analyzers mentioned above there is no compile-time dependency management - I think it's assumed each analyzer is independently loadable
That problem exists in any situation. Type provider design-time components (let's call those TPDTCs) are loaded into host tooling using Assembly.LoadFrom (regardless of how they are specified and found). There is no other choice (unless we either isolate them in a process - AppDomains are no longer an option on .NET Core). This causes problems - for example, TPDTCs may also not have binding redirects (except ones implemented via adhoc AssemblyResolve events, and ones provided by host tooling, e.g. a redirect to a recent version of FSharp.Core can generally be assumed) . I don't think Paket and Nuget can help us here - how will Paket know of the binding configuration of Ionide, or VS2019, or FSC, or ... , which I believe it needs to know to do useful dependency management of compile-time addins. Perhaps in the longer term we can run TPDTCs isolated, e.g. in a separate process (cf. FsAutoComplete.exe). However it is not totally easy to remote the System.Type/Method/... API through a process boundary. Perhaps there is more we can learn from how Roslyn analyzers are loaded.
The good news is that in practice the design-time component of most TPs can be relatively simple .NET Standard 2.0 components (e.g. this works for FSharp.Data). There are cases where the component has significant non-.NET Standard dependencies (e.g. RProvider) - over time we really just have to hope that these dependencies become available as .NET Standard components. |
I don't think this solves type provider references. Say that separate TPDTCs are available for .NET Framework 4.5 and .NET Standard 2.0. Which type TPDTC reference is emitted into the load script when running in FSI.EXE (.NET Framework 4.6 32 bit)? What about when type checking in devenv.exe (.NET Framework 32 bit)? When running in the F# Compiler (.NET Core 2.0 64 bit)? The package manager library (Paket.Core.dll) would have to look at the configuration of the tool it is running in (i.e,. FSI.EXE on .NET Core 2.0 x64) and do resolution based on that, but again presumably looking under some equivalent of the But that's just adding some fairly bespoke resolution rules into Paket. And we'd need another implementation in MSBuild/nuget. I understand there's some perfect world where the compiler relies on external tooling to tell it about TPDTCs. But relying on external tools causes a lot of procedural problems too, and I'm not sure those tools know that much that's useful, apart from the framework compatibility relationship - which is trivial if all we care about is netstandard2.0, netcoreapp2.0 and net4x. Note that we made a decision not to rely on external tooling for the resolution of TPDTCs in 2012 - and given the vast churn in nuget/msbuild since then that feels like a wise decision - it feels to me that external package manager tooling actually churns faster than anything else in the dotnettyverse :) - and any bespoke feature we might have put in back then would have been waaaay out of date by now. Better to have control over our destiny and put the rules in the F# compiler.. :) One reason I'm in favour of the proposal above is that it represents a small variation on where we are today: just 20-50 lines of code in ExtensionTyping.fs, and I think it will get us to a stable place for the next 2-3 years. If we are going to change things in a major way I feel we should be more radical, e.g. look at interpreting type providers, or isolating them, or ... Basically getting rid of type providers as binary components. But that doesn't solve dependencies and so on, unless we use a full IL interpreter. (Also, we could look at what Scala tooling does for macros - the problems are very similar and I know there has been a lot of engineering challenges there). |
Yes please let's figure out their solution and go down the same/a similar route. Also it might be that we want to support language agnostic analyzers sooner or later one way or another, but in the mean-time we could even use the unused "fsharp" folder in there: I'll try to respond to the rest of your response as well (as it contains valid critics) but I just wanted to get this part out. |
TPDTCs are not analyzers, so putting them in the analyzers directory seems problematic. Adding "fsharp" to the path makes sense I should also add this: when executing the F# compiler with .NET Core TPDTCs are loaded using |
@dsyme, can we just use a nuget package to ship the design assemblies? <Compile Include="$(ProvidedTypesSourcePath)" /> the |
@enricosada No - that would mean the TPDTCs get referenced as normal assemblies ( |
We can be pretty fast in adding and releasing support into Paket. So that point is not valid. Can't speak for NuGet, but if we decide to re-use analyzers infrastructure nobody needs to do anything besides F# SDK and compiler?
Paket doesn't have to know anything, because load scripts can add
I don't see the reason why we cannot do the same (ie. say that type providers need to be standalone) or how that is related to adding magic path resolution to the F# compiler.
The "Paket"-Plugin in the compiler can figure out the host it is running on and get the correct references. At least that is how I understand @forki's suggested solution. So I don't see the problem. We already have RID resolution support for runtime dependencies in Paket so we could reuse that once we have decided how we want to package TCs with such special requirements.
:/
Or they would have considered the requirement from the start in the new world or abstracted the idea of analyzers into compiler plugins. Nobody knows.
Maybe, Maybe not.
Yep they need a redesign badly, but as you point out it doesn't really solve this problem in any way (I don't understand how a full IL interpreter would help here?)
To be honest I don't really think it is. In fact it probably should have been considered as compiler plugins from the beginning. We might even consider to use
Actually they probably should be loaded into their own loadcontext, then a shared TPDTC with different versions for two type providers could technically work. That "might" be a reason to resolve type provider design time dependencies in the compiler (Though I think it can work via command line as well, it is just a bit more difficult to implement correctly). Honestly, I don't think this will be changed at all so it isn't actually a reason in practice.
Yes from a quick research it seems like in fact all roslyn-analyzers are self-contained as well (ie. they bundle all dependencies). Threfore, I'd suggest (as a first step) to go down the same route and ignore all issues a design time resolution would introduce. |
Just to note that for F# TPDTCs, it appears to make sense to be able to qualify on the F# version number of the host tooling. For example
Possibly also other monikers for Fable tooling etc. For each such version of F# tooling, we would specify a minimum base FSharp.Core that you can assume is minimal for that host tooling, e.g.
and so on. Each TPDTC can be written with respect to the its appropriate FSharp.Core or earlier. To motivate, FSharp.Core 4.0.0.0 included new contracts for TPDTCs for static parameters on methods which weren't present in 4.3.1.0. But a type provider that wanted to be usable in F# 3.1 tooling couldn't make use of that, and in practice almost no one is using that feature partly for that reason. Making this mandatory would help allow us to gradually progress the type provider API and to improve the API, extending it to include more capabilities and perhaps even adding a revamped, new API altogether, but still have packages which ship TPDTC components which work with previous-generation tooling. It's partly because I want F# to have control over this sort of dimension to versioning that I'm not totally happy with relying on external tools to do resolution with respect to runtime assumptions. (I assume Roslyn analyzers will need similar qualification based on roslyn version, but that the solution grid may be different) I'm a big fan of continuing to extend the TPDTC contracts in interesting directions but in order to do that sanely we will need some scheme like this. |
Closing this, as the RFC implementation is merged and we're entering the final stages of this work soon |
I'm trying to build a project that consumes Example 3 type provider (.NET Standard design-time DLL) using .NET Core SDK command line. It builds just fine if I apply |
@dsyme regarding the example The package I'm looking at has four native assemblies win-x64, win-x86, win-arm and win-64 but the example only shows two x86 and x64. |
My understanding is that the F# tooling doesn't know anything about these names - rather it does the Assembly.Load from the chosen location and the varsious versions of the .NET Runtime can in some circumstances look in the subdirectories for native components. However I'm not certain of the details and it doesn't appear to be under test. In general TPDTC with native dependencies should just be avoided to be honest - your TP may need to load into all sorts of F# tooling on different architectures |
@dsyme thanks for the information, although this might be bad news for me and anyone making database type providers. My specific situation is building a type provider for database access using Microsoft.Data.SqlClient. On windows (unix is all managed code) this package has a dependency on Microsoft.Data.SqlClient.SNI.runtime. I haven't seen a NuGet package like this one before, it only contains native binaries, it's structure looks like this.
I can successfully load If I cannot load native a dll, then it is currently not possible to write a TP for
Do you have any recommendations regarding these options? |
As a third workaround (not sustainable solution), @RonaldSchlenker, what you can do is unload the AssemblyResolve event handler on AppDomain.CurrentDomain before Assembly load call and replace it with your custom function... will involve some hours of frustrating VS debugging. |
Thanks @Thorium for the suggested workaround. I was one of the people pinged by @WillEhrendreich in the related issue, so I thought I’ll try to remember the infos I had when I was confronted with that issue (don’t even know where I posted my comments). But it’s currently not an issue that I’m facing. Anyway, this morning I tried to understand why the SqlClient (the „new“ one) is not able to correctly resolve the native parts that actually match the runtime. In normally compiled apps (net461, net6), it doesn’t seem to be an issue. Perhaps (and maybe the commenters before me already mentioned it), it’s a combination of - host default resolve behavior - TP specialties - MsSqlClient lib resolve behavior - others, but I have no deeper insight on that, and I’m not a TP expert. To get some more knowledge, I’ve created a stripped-down TP, a very simple one, that logs infos concerning compile time (IDE) and runtime, and does a simple SQL query using MsSqlClient. It’s not ready yet, but might be helpful to understand a bit more and hopefully find a stable way for solving this or related issues. I’ll post more infos in the related issues. The repo of the mentioned TP is here: https://github.com/RonaldSchlenker/DebuggingTypeProvider |
Too curious not to dig in. Thanks, @RonaldSchlenker , I've forked it already, lol. |
To follow up to my previous comment You can avoid loading the native SNI by switching to managed SNI on Windows:
|
Without wanting to be rude, I have to say that creating your own type providers is far too complicated. Unfortunately, the process is completely non-transparent with incomprehensible error messages. |
There are some plans to revamp them at some point and make the sdk part of compiler, but it didn't make it for next release unfortunately. |
That would be great! |
Recent PRs in the Type Provider SDK repo (e.g. fsprojects/FSharp.TypeProviders.SDK#139) are a big step towards completing our type provider story for .NET Core With this work, properly written type providers using the latest version of the TPSDK can now always produce target code for either .NET Core or .NET Framework – i.e. they properly adjust for the target reference assemblies being used by the compilation. This applies to both generative and erasing type providers.
One final piece of the type provider puzzle is to have the F# compiler load an appropriate design-time DLL depending on whether the compiler is running in .NET Core or .NET Framework.
Currently, the compiler looks for design time DLL alongside the referenced runtime DLLs. (For simple type providers, these DLLs are the same)
We will do something like what is specified her https://docs.microsoft.com/en-us/nuget/schema/analyzers-conventions,
Rough proposal
When a referenced assembly as the usual TypeProviderAssembly attribute, indicating it wants design-time type provider component “MyDesignTime.dll”, then
relative to the location of the runtime DLL, which is presumed to be in a nuget package.
When we use
...
we mean a recursive upwards directory search looking for a directory namestypeproviders
, stopping when we find a directory namepackages
WHen we use
fsharpNN
we mean a successive search backwards forfsharp42
,fsharp41
etc. Putting a TPDTC infsharp41
means the TPDTC is suitable to load into F# 4.1 tooling and later, and has the right to minimally assume FSharp.Core 4.4.1.0Some examples:
The idea is that a package can contain a type provider design-time for both .NET Core and .NET Framework. This will allow it to load into both compiler, F# Interactive and tools running .NET Framework OR .NET Core.
Example 1 - the ultimate simplest form
Going forward, the very simplest type providers will eventually use a single .NET Standard 2.0 DLL for both the runtime library and the design-time component. Layout:
As of today however you will also need to ship facades for netstandard.dll, System.Runtime.dll and System.Reflection.dll alongside this component and as such you should either
lib
directory, ORExample 2 - FSharp.Data
FSharp.Data would lay out as follows:
Here we are assuming FSharp.Data wants to use different design time DLLs for the two host tooling contexts we care about. Example 3 deals with the case where FSHarp.Data wants to use a single DLL.
Example 3 - FSharp.Data (with .NET Standard design-time DLL)
A layout like this may also be feasible if shipping facades to create a .NET Standard 2.0 TPDTC
See note on facades above.
Example 4 - type providers with 32/64-bit dependencies
A Python type provider may have different dependencies for 32 and 64-bit for both runtime and design-time (the directory names may not be exactly right)
plus facades as mentioned above
Non Example 5
Going forward, we should not be happy with type provider packages that look like this - these will be unusable when the compiler executes using .NET Core
or even
Again if your TPDTC component is a solitary NET Framework component then it will be unusable when a host tool executes using .NET Core.
Remember, we must be able to load type provider design-time DLLs into many different tooling contexts, notable
and any context that uses FSharp.Compiler.Service.dll as either netstandard 2.0 or a .NET Framework component.
Note the above is effectively a proposed package architecture for any cross-generating compiler plugins. If we ever support other kinds of compiler plugins then we could follow a similar pattern.
The text was updated successfully, but these errors were encountered: