WTSC 4.3 Build: Translating Actions into Jobs using a Toolchain

131 阅读3分钟

In the previous post, we know how the compiler driver construct actions, after that, it will call the swift::Driver::buildJobs method to translate actions into jobs using a toolchain.

/* /wtsc/swift/lib/Driver/Driver.cpp */
...
std::unique_ptr<Compilation>
Driver::buildCompilation(const ToolChain &TC,
                         std::unique_ptr<llvm::opt::InputArgList> ArgList) {
...
  // Construct the graph of Actions.
  SmallVector<const Action *, 8> TopLevelActions;
  buildActions(TopLevelActions, TC, OI,
               whyIgnoreIncrementallity.empty() ? &outOfDateMap : nullptr, *C);
...
  buildJobs(TopLevelActions, OI, OFM ? OFM.getPointer() : nullptr,
            workingDirectory, TC, *C);
...
}
...

The swift::Driver::buildJobs method loops over each top level action and passes any JobAction to swift::Driver::buildJobsForAction method. Because the InputAction need not a job (an invocation of tool in toolchain) to implement it.

/* /wtsc/swift/lib/Driver/Driver.cpp */
...
void Driver::buildJobs(ArrayRef<const Action *> TopLevelActions,
                       const OutputInfo &OI, const OutputFileMap *OFM,
                       StringRef workingDirectory, const ToolChain &TC,
                       Compilation &C) const {
 ...
  for (const Action *A : TopLevelActions) {
    if (auto *JA = dyn_cast<JobAction>(A)) {
      (void)buildJobsForAction(C, JA, OFM, workingDirectory, /*TopLevel=*/true,
                               JobCache);
    }
  }
}
...

The swift::Driver::buildJobsForAction method does one core stuff, to constuction the Job for a given action through function parameter, and has five steps to do that, as comments inside its function body.

/* /wtsc/swift/lib/Driver/Driver.cpp */
...
Job *Driver::buildJobsForAction(Compilation &C, const JobAction *JA,
                                const OutputFileMap *OFM,
                                StringRef workingDirectory,
                                bool AtTopLevel, JobCacheMap &JobCache) const {
// 1. See if we've already got this cached.
  const ToolChain &TC = C.getToolChain();
  std::pair<const Action *, const ToolChain *> Key(JA, &TC);
  {
    auto CacheIter = JobCache.find(Key);
    if (CacheIter != JobCache.end()) {
      return CacheIter->second;
    }
  }

  // 2. Build up the list of input jobs.
  SmallVector<const Action *, 4> InputActions;
  SmallVector<const Job *, 4> InputJobs;
  for (const Action *Input : *JA) {
    if (auto *InputJobAction = dyn_cast<JobAction>(Input)) {
      InputJobs.push_back(buildJobsForAction(
          C, InputJobAction, OFM, workingDirectory, false, JobCache));
    } else {
      InputActions.push_back(Input);
    }
  }

  // 3. Determine the CommandOutput for the job.
  ...
  const OutputInfo &OI = C.getOutputInfo();
  ...
  std::unique_ptr<CommandOutput> Output(
      new CommandOutput(JA->getType(), C.getDerivedOutputFileMap()));
  ...

  // 4. Construct a Job which produces the right CommandOutput.
  std::unique_ptr<Job> ownedJob = TC.constructJob(*JA, C, std::move(InputJobs),
                                                  InputActions, 
                                                  std::move(Output), OI);
  Job *J = C.addJob(std::move(ownedJob)); 
  ...
  
  // 5. Add it to the JobCache, so we don't construct the same Job multiple
  // times.
  JobCache[Key] = J;
...
}
...

The step one and five are about caching job so that it can reduce the duplication of the same job. Thus we can ignore these two steps now to narraw down what is significant for our purpose, converting actions to jobs.

The second step is to provide the inputs of the required job. And the third step is to construct an CommandOutput to describe what needs to produce for the required job.

Then the step four is going to use the given JobAction, inputs from step two, output description from step three, and the Compilation environment to construct a Job to complete the required action. That is given inputs, related information, and the output description, please make a job that can produce the required output.

Next, let us see how toolchain make that job by checking out swift::driver::ToolChain::constructJob method.

/* /wtsc/swift/lib/Driver/ToolChain.cpp */
...
std::unique_ptr<Job> ToolChain::constructJob(
    const JobAction &JA, Compilation &C, SmallVectorImpl<const Job *> &&inputs,
    ArrayRef<const Action *> inputActions,
    std::unique_ptr<CommandOutput> output, const OutputInfo &OI) const {
...
    auto invocationInfo = [&]() -> InvocationInfo {
    switch (JA.getKind()) {
#define CASE(K)                                                                \
  case Action::Kind::K:                                                        \
    return constructInvocation(cast<K##Action>(JA), context);
      CASE(CompileJob)
      CASE(InterpretJob)
      CASE(BackendJob)
      CASE(MergeModuleJob)
      CASE(ModuleWrapJob)
      CASE(DynamicLinkJob)
      CASE(StaticLinkJob)
      CASE(GenerateDSYMJob)
      CASE(VerifyDebugInfoJob)
      CASE(GeneratePCHJob)
      CASE(AutolinkExtractJob)
      CASE(REPLJob)
      CASE(VerifyModuleInterfaceJob)
#undef CASE
    case Action::Kind::Input:
      llvm_unreachable("not a JobAction");
    }

    // Work around MSVC warning: not all control paths return a value
    llvm_unreachable("All switch cases are covered");
  }();
...
    return std::make_unique<Job>(
      JA, std::move(inputs), std::move(output), executablePath,
      std::move(invocationInfo.Arguments),
      std::move(invocationInfo.ExtraEnvironment),
      std::move(invocationInfo.FilelistInfos), responseFileInfo);
...
}
...

In order to translate an action into a job, the ToolChain uses a macro that calls the appropriate ToolChain::constructInvocation method for each action class to product an instance of InvocationInfo to collect information chosen by toolchains to create jobs. 

/* /wtsc/swift/include/swift/Driver/ToolChain.h */
...
/// Packs together information chosen by toolchains to create jobs.
  struct InvocationInfo {
    const char *ExecutableName;
    llvm::opt::ArgStringList Arguments;
    std::vector<std::pair<const char *, const char *>> ExtraEnvironment;
    std::vector<FilelistInfo> FilelistInfos;

    // Not all platforms and jobs support the use of response files, so assume
    // "false" by default. If the executable specified in the InvocationInfo
    // constructor supports response files, this can be overridden and set to
    // "true".
    bool allowsResponseFiles = false;

    InvocationInfo(const char *name, llvm::opt::ArgStringList args = {},
                   decltype(ExtraEnvironment) extraEnv = {})
        : ExecutableName(name), Arguments(std::move(args)),
          ExtraEnvironment(std::move(extraEnv)) {}
  };
...

The ToolChain classes overload constructionInvocation in order to execute different logic for different kind of action on different platform to consturct platform-specific command line invocations for each action. If some actions are the same for all supported platforms, they are hanled in the base ToolChain class.

After we get that InvocationInfo, we can construct a job for the given action. A job is an object that store all the needing information to tell on the specific what command line it needs to invoke with what arguments, when to invoke, the relation with other jobs, which action that job is from, and so on.

The jobs converted from actions are stored in a flat list in the instance of swift::driver::Compilation.

Job *J = C.addJob(std::move(ownedJob));

Later on, the compilation instance will perform jobs on a task queue.