navigation

コンパイラサービス: ファイルシステム仮想化

FSharp.Compiler.Service にはファイルシステムを表すグローバル変数があります。 この変数を設定するこにより、ファイルシステムが利用できない状況でも コンパイラをホストすることができるようになります。

注意: 以下で使用しているAPIは実験的なもので、 新しいnugetパッケージの公開に伴って変更される可能性があります。

FileSystemの設定

以下の例ではディスクからの読み取りを行うような実装をファイルシステムに設定しています:

#r "FSharp.Compiler.Service.dll"
open System
open System.IO
open System.Collections.Generic
open System.Text
open FSharp.Compiler.AbstractIL.Internal.Library

let defaultFileSystem = Shim.FileSystem

let fileName1 = @"c:\mycode\test1.fs" // 注意: 実際には存在しないファイルのパス
let fileName2 = @"c:\mycode\test2.fs" // 注意: 実際には存在しないファイルのパス

type MyFileSystem() =
    let file1 = """
module File1

let A = 1"""
    let file2 = """
module File2
let B = File1.A + File1.A"""
    let files = dict [(fileName1, file1); (fileName2, file2)]

    interface IFileSystem with
        // 読み取りおよび書き込み用にファイルをオープンする機能を実装
        member __.FileStreamReadShim(fileName) =
            match files.TryGetValue fileName with
            | true, text -> new MemoryStream(Encoding.UTF8.GetBytes(text)) :> Stream
            | _ -> defaultFileSystem.FileStreamReadShim(fileName)

        member __.FileStreamCreateShim(fileName) =
            defaultFileSystem.FileStreamCreateShim(fileName)

        member __.IsStableFileHeuristic(fileName) =
            defaultFileSystem.IsStableFileHeuristic(fileName)

        member __.FileStreamWriteExistingShim(fileName) =
            defaultFileSystem.FileStreamWriteExistingShim(fileName)

        member __.ReadAllBytesShim(fileName) =
            match files.TryGetValue fileName with
            | true, text -> Encoding.UTF8.GetBytes(text)
            | _ -> defaultFileSystem.ReadAllBytesShim(fileName)

        // 一時パスおよびファイルのタイムスタンプに関連する機能を実装
        member __.GetTempPathShim() =
            defaultFileSystem.GetTempPathShim()

        member __.GetLastWriteTimeShim(fileName) =
            defaultFileSystem.GetLastWriteTimeShim(fileName)

        member __.GetFullPathShim(fileName) =
            defaultFileSystem.GetFullPathShim(fileName)

        member __.IsInvalidPathShim(fileName) =
            defaultFileSystem.IsInvalidPathShim(fileName)

        member __.IsPathRootedShim(fileName) =
            defaultFileSystem.IsPathRootedShim(fileName)

        // ファイルの存在確認および削除に関連する機能を実装
        member __.SafeExists(fileName) =
            files.ContainsKey(fileName) || defaultFileSystem.SafeExists(fileName)

        member __.FileDelete(fileName) =
            defaultFileSystem.FileDelete(fileName)

        // アセンブリのロードに関連する機能を実装。
        // 型プロバイダやF# Interactiveで使用される。
        member __.AssemblyLoadFrom(fileName) =
            defaultFileSystem.AssemblyLoadFrom fileName

        member __.AssemblyLoad(assemblyName) =
            defaultFileSystem.AssemblyLoad assemblyName

let myFileSystem = MyFileSystem()
Shim.FileSystem <- MyFileSystem()

FileSystemによるコンパイルの実行

open FSharp.Compiler.SourceCodeServices

let checker = FSharpChecker.Create()
let projectOptions =
    let allFlags =
        [| yield "--simpleresolution";
           yield "--noframework";
           yield "--debug:full";
           yield "--define:DEBUG";
           yield "--optimize-";
           yield "--doc:test.xml";
           yield "--warn:3";
           yield "--fullpaths";
           yield "--flaterrors";
           yield "--target:library";
           let references =
             [ @"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.dll";
               @"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.dll";
               @"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Core.dll";
               @"C:\Program Files (x86)\Reference Assemblies\Microsoft\FSharp\.NETFramework\v4.0\4.3.0.0\FSharp.Core.dll"]
           for r in references do
                 yield "-r:" + r |]

    { ProjectFileName = @"c:\mycode\compilation.fsproj" // 現在のディレクトリで一意な名前を指定
      ProjectId = None
      SourceFiles = [| fileName1; fileName2 |]
      OriginalLoadReferences = []
      ExtraProjectInfo=None
      Stamp = None
      OtherOptions = allFlags
      ReferencedProjects=[| |]
      IsIncompleteTypeCheckEnvironment = false
      UseScriptResolutionRules = true
      LoadTime = System.DateTime.Now // 'Now' を指定して強制的に再読込させている点に注意
      UnresolvedReferences = None }

let results = checker.ParseAndCheckProject(projectOptions) |> Async.RunSynchronously

results.Errors
results.AssemblySignature.Entities.Count //2
results.AssemblySignature.Entities.[0].MembersFunctionsAndValues.Count //1
results.AssemblySignature.Entities.[0].MembersFunctionsAndValues.[0].DisplayName // "B"

まとめ

このチュートリアルでは FSharp.Compiler.Service コンポーネントで使用される ファイルシステムに注目して、グローバルな設定を変更する方法について紹介しました。

このチュートリアルの執筆時点では、以下に列挙したSystem.IOの操作に対しては 仮想化されたファイルシステムAPIが用意されない予定になっています。 将来のバージョンのコンパイラサービスではこれらのAPIが追加されるかもしれません。

  • Path.Combine
  • Path.DirectorySeparatorChar
  • Path.GetDirectoryName
  • Path.GetFileName
  • Path.GetFileNameWithoutExtension
  • Path.HasExtension
  • Path.GetRandomFileName (アセンブリ内にコンパイル済みwin32リソースを生成する場合にのみ使用される)

注意: SourceCodeServices API内の一部の操作では、 引数にファイルの内容だけでなくファイル名を指定する必要があります。 これらのAPIにおいて、ファイル名はエラーの報告のためだけに使用されます。

注意: 型プロバイダーコンポーネントは仮想化されたファイルシステムを使用しません。

注意: コンパイラサービスは --simpleresolution が指定されていない場合、 MSBuildを使ってアセンブリの解決を試みることがあります。 FileSystem APIを使用する場合、通常はコンパイラへのフラグとして --simpleresolution を指定することになります。 それと同時に --noframework を指定します。 .NETアセンブリに対するすべての参照を明示的に指定する必要があるでしょう。

namespace System
namespace System.IO
namespace System.Collections
namespace System.Collections.Generic
namespace System.Text
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
namespace FSharp.Compiler
namespace FSharp.Compiler.AbstractIL
namespace FSharp.Compiler.AbstractIL.Internal
module Library

from FSharp.Compiler.AbstractIL.Internal
val defaultFileSystem : IFileSystem
module Shim

from FSharp.Compiler.AbstractIL.Internal.Library
val mutable FileSystem : IFileSystem
val fileName1 : string
val fileName2 : string
Multiple items
type MyFileSystem =
  interface IFileSystem
  new : unit -> MyFileSystem

--------------------
new : unit -> MyFileSystem
val file1 : string
val file2 : string
val files : IDictionary<string,string>
val dict : keyValuePairs:seq<'Key * 'Value> -> IDictionary<'Key,'Value> (requires equality)
type IFileSystem =
  interface
    abstract member AssemblyLoad : assemblyName:AssemblyName -> Assembly
    abstract member AssemblyLoadFrom : fileName:string -> Assembly
    abstract member FileDelete : fileName:string -> unit
    abstract member FileStreamCreateShim : fileName:string -> Stream
    abstract member FileStreamReadShim : fileName:string -> Stream
    abstract member FileStreamWriteExistingShim : fileName:string -> Stream
    abstract member GetFullPathShim : fileName:string -> string
    abstract member GetLastWriteTimeShim : fileName:string -> DateTime
    abstract member GetTempPathShim : unit -> string
    abstract member IsInvalidPathShim : filename:string -> bool
    ...
  end
val fileName : string
val text : string
Multiple items
type MemoryStream =
  inherit Stream
  new : unit -> MemoryStream + 6 overloads
  member CanRead : bool
  member CanSeek : bool
  member CanWrite : bool
  member Capacity : int with get, set
  member CopyTo : destination:Stream * bufferSize:int -> unit
  member CopyToAsync : destination:Stream * bufferSize:int * cancellationToken:CancellationToken -> Task
  member Flush : unit -> unit
  member FlushAsync : cancellationToken:CancellationToken -> Task
  member GetBuffer : unit -> byte[]
  ...

--------------------
MemoryStream() : MemoryStream
MemoryStream(capacity: int) : MemoryStream
MemoryStream(buffer: byte []) : MemoryStream
MemoryStream(buffer: byte [], writable: bool) : MemoryStream
MemoryStream(buffer: byte [], index: int, count: int) : MemoryStream
MemoryStream(buffer: byte [], index: int, count: int, writable: bool) : MemoryStream
MemoryStream(buffer: byte [], index: int, count: int, writable: bool, publiclyVisible: bool) : MemoryStream
type Encoding =
  member BodyName : string
  member Clone : unit -> obj
  member CodePage : int
  member DecoderFallback : DecoderFallback with get, set
  member EncoderFallback : EncoderFallback with get, set
  member EncodingName : string
  member Equals : value:obj -> bool
  member GetByteCount : chars:char[] -> int + 5 overloads
  member GetBytes : chars:char[] -> byte[] + 7 overloads
  member GetCharCount : bytes:byte[] -> int + 3 overloads
  ...
property Encoding.UTF8: Encoding with get
type Stream =
  inherit MarshalByRefObject
  member BeginRead : buffer:byte[] * offset:int * count:int * callback:AsyncCallback * state:obj -> IAsyncResult
  member BeginWrite : buffer:byte[] * offset:int * count:int * callback:AsyncCallback * state:obj -> IAsyncResult
  member CanRead : bool
  member CanSeek : bool
  member CanTimeout : bool
  member CanWrite : bool
  member Close : unit -> unit
  member CopyTo : destination:Stream -> unit + 1 overload
  member CopyToAsync : destination:Stream -> Task + 3 overloads
  member Dispose : unit -> unit
  ...
val __ : MyFileSystem
val assemblyName : Reflection.AssemblyName
val myFileSystem : MyFileSystem
namespace FSharp.Compiler.SourceCodeServices
val checker : FSharpChecker
type FSharpChecker =
  member CheckFileInProject : parsed:FSharpParseFileResults * filename:string * fileversion:int * sourceText:ISourceText * options:FSharpProjectOptions * ?textSnapshotInfo:obj * ?userOpName:string -> Async<FSharpCheckFileAnswer>
  member CheckProjectInBackground : options:FSharpProjectOptions * ?userOpName:string -> unit
  member ClearCache : options:seq<FSharpProjectOptions> * ?userOpName:string -> unit
  member ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients : unit -> unit
  member Compile : argv:string [] * ?userOpName:string -> Async<FSharpErrorInfo [] * int>
  member Compile : ast:ParsedInput list * assemblyName:string * outFile:string * dependencies:string list * ?pdbFile:string * ?executable:bool * ?noframework:bool * ?userOpName:string -> Async<FSharpErrorInfo [] * int>
  member CompileToDynamicAssembly : otherFlags:string [] * execute:(TextWriter * TextWriter) option * ?userOpName:string -> Async<FSharpErrorInfo [] * int * Assembly option>
  member CompileToDynamicAssembly : ast:ParsedInput list * assemblyName:string * dependencies:string list * execute:(TextWriter * TextWriter) option * ?debug:bool * ?noframework:bool * ?userOpName:string -> Async<FSharpErrorInfo [] * int * Assembly option>
  member FindBackgroundReferencesInFile : filename:string * options:FSharpProjectOptions * symbol:FSharpSymbol * ?canInvalidateProject:bool * ?userOpName:string -> Async<seq<range>>
  member GetBackgroundCheckResultsForFileInProject : filename:string * options:FSharpProjectOptions * ?userOpName:string -> Async<FSharpParseFileResults * FSharpCheckFileResults>
  ...
static member FSharpChecker.Create : ?projectCacheSize:int * ?keepAssemblyContents:bool * ?keepAllBackgroundResolutions:bool * ?legacyReferenceResolver:FSharp.Compiler.ReferenceResolver.Resolver * ?tryGetMetadataSnapshot:FSharp.Compiler.AbstractIL.ILBinaryReader.ILReaderTryGetMetadataSnapshot * ?suggestNamesForErrors:bool * ?keepAllBackgroundSymbolUses:bool * ?enableBackgroundItemKeyStoreAndSemanticClassification:bool -> FSharpChecker
val projectOptions : FSharpProjectOptions
val allFlags : string []
val references : string list
val r : string
union case Option.None: Option<'T>
Multiple items
type DateTime =
  struct
    new : ticks:int64 -> DateTime + 10 overloads
    member Add : value:TimeSpan -> DateTime
    member AddDays : value:float -> DateTime
    member AddHours : value:float -> DateTime
    member AddMilliseconds : value:float -> DateTime
    member AddMinutes : value:float -> DateTime
    member AddMonths : months:int -> DateTime
    member AddSeconds : value:float -> DateTime
    member AddTicks : value:int64 -> DateTime
    member AddYears : value:int -> DateTime
    ...
  end

--------------------
DateTime ()
   (+0 other overloads)
DateTime(ticks: int64) : DateTime
   (+0 other overloads)
DateTime(ticks: int64, kind: DateTimeKind) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, calendar: Globalization.Calendar) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: DateTimeKind) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: Globalization.Calendar) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, kind: DateTimeKind) : DateTime
   (+0 other overloads)
property DateTime.Now: DateTime with get
val results : FSharpCheckProjectResults
Multiple items
type Async =
  static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
  static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
  static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
  static member AwaitTask : task:Task -> Async<unit>
  static member AwaitTask : task:Task<'T> -> Async<'T>
  static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
  static member CancelDefaultToken : unit -> unit
  static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
  static member Choice : computations:seq<Async<'T option>> -> Async<'T option>
  static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
  ...

--------------------
type Async<'T> =
static member Async.RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:Threading.CancellationToken -> 'T