bytecodealliance / wasmtime-cpp

Apache License 2.0
86 stars 18 forks source link

[Idea] WasiApp #25

Open redradist opened 2 years ago

redradist commented 2 years ago

Hi all,

Recently I wrote small wrapper that simplify loading and linking wasi application with all dependencies, see example:

  // Create WasiApp that encapsulate all complexity for linking underhood
  WasiApp wasi_app(linkdirs, workdir);

  // Configure WASI and store it within our `wasmtime_store_t`
  ws::WasiConfig wasi;
  wasi.inherit_argv();
  wasi.inherit_env();
  wasi.inherit_stdin();
  wasi.inherit_stdout();
  wasi.inherit_stderr();

  // Set config to WasiApp
  wasi_app.set_config(std::move(wasi));
  // Load main application with all dependencies
  wasi_app.load_app(app_name);
  // Run application with entry point 'run'
  wasi_app.run("run", {});

The benefits is that this class encapsulate all complexity for properly loading application. For example, underhood wasi_app.load_app(app_name) loads all dependencies that are describe in import sections

Do you think such functionality would be useful in this repo ? (I could create PR) Or do you think it should be provided as functionality from wasmtime ? Or not needed at all ?

Please, share your ideas ...

alexcrichton commented 2 years ago

Personally I think it's best to stick pretty close to the C API without adding too many extra layers of abstraction, but if you'd like I think this would probably make for a good example program?

redradist commented 2 years ago

Okay, I mean that using C-API it is hard to link big program consistent with some pieces 100 wasm files ... wasi_app.load_app(app_name); just simplifies it

redradist commented 2 years ago

@alexcrichton One additional question, could you explain of provide link to documentation for case where wasmtime loads application with runtime dependencies, for example:

wasmtime some_app.wasm # It depends on library.wasm that located in the same directory

The question, how to provide to wasmtime path to this dependency ?

alexcrichton commented 2 years ago

Sorry I'm not sure what you mean by "pieces 100 wasm files" or what you mean by the path dependencies that wasmtime loads. The CLI of the wasmtime executable is quite primitive and it doesn't really work for any nontrivial wasms in terms of dependencies, the only thing that automatically works is WASI itself.

redradist commented 2 years ago

@alexcrichton Lets see my example for WasiApp:

class WasiApp {
 public:
  explicit WasiApp(std::vector<fs::path> linkdirs,
                   std::optional<fs::path> workdir = std::nullopt)
      : link_dirs_{std::move(linkdirs)}
      , workdir_{std::move(workdir)} {
    linker_.define_wasi().unwrap();
  }

  void set_config(ws::WasiConfig&& config) {
    store_.context().set_wasi(std::move(config)).unwrap();
  }

  void load_app(const std::string& app_name) {
    reset_app();
    auto workdir = workdir_.value_or(fs::current_path());
    std::optional<fs::path> filename = find_file(workdir, app_name);
    ws::Module module = compile(filename.value());
    app_instance_ = link(app_name, module);
  }

  void run(const std::string& entry_point, const std::vector<ws::Val> &params) {
    ws::Func f = std::get<ws::Func>(*app_instance_.value().get(store_, entry_point));
    f.call(store_, {}).unwrap();
  }

 private:
  [[nodiscard]]
  static std::string read_wat_file(const char* name) {
    std::ifstream watFile;
    watFile.open(name);
    std::stringstream strStream;
    strStream << watFile.rdbuf();
    return strStream.str();
  }

  [[nodiscard]]
  static std::vector<uint8_t> read_wasm_file(const char* name) {
    std::ifstream bin_file(name, std::ios::binary);
    return {(std::istreambuf_iterator<char>(bin_file)), (std::istreambuf_iterator<char>())};
  }

  [[nodiscard]]
  static std::optional<fs::path> find_file(const fs::path& search_dir,
                                           const std::string& app_name) {
    std::optional<fs::path> filename;
    for (auto &p : fs::directory_iterator(search_dir)) {
      if (is_regular_file(p.path())) {
        if (p.path().stem() == app_name &&
            (p.path().extension() == ".wasm" ||
             p.path().extension() == ".wat")) {
          filename = p.path();
          break;
        }
      }
    }
    return filename;
  }

  [[nodiscard]]
  ws::Instance link(const std::string& name, const ws::Module& module) {
    std::unordered_map<std::string, std::vector<std::string>> imports;
    for (auto i : module.imports()) {
      imports[std::string(i.module())].push_back(std::string(i.name()));
    }
    for (auto& [import_file, import_symbols] : imports) {
      std::sort(import_symbols.begin(), import_symbols.end());
    }
    load_imports(imports);
    ws::Instance linking_instance = linker_.instantiate(store_, module).unwrap();
    linker_.define_instance(store_, name, linking_instance);
    imported_modules_.push_back(module);
    return linking_instance;
  }

  [[nodiscard]]
  ws::Module compile(const fs::path& path) {
    if (path.extension() == ".wat") {
      auto linking_wat = read_wat_file(path.string().c_str());
      return ws::Module::compile(engine_, linking_wat).unwrap();
    } else {
      auto linking_wasm = read_wasm_file(path.string().c_str());
      return ws::Module::compile(engine_, linking_wasm).unwrap();
    }
  }

  void load_imports(const std::unordered_map<std::string, std::vector<std::string>>& imports) {
    for (const auto& [import_file, import_symbols] : imports) {
      if (import_file.find("wasi_snapshot") != std::string::npos) {
        continue;
      }
      bool is_found_wasm = false;
      for (const auto& link_dirs : link_dirs_) {
        std::optional<fs::path> link_filename = find_file(link_dirs, import_file);
        if (link_filename) {
          ws::Module linking_module = compile(link_filename.value());
          std::vector<std::string> exports;
          for (const auto& e : linking_module.exports()) {
            exports.emplace_back(e.name());
          }
          std::sort(exports.begin(), exports.end());
          if (exports != import_symbols) {
            continue;
          }
          ws::Instance linking_instance = link(import_file, linking_module);
          is_found_wasm = true;
          break;
        }
      }
      if (!is_found_wasm) {
        fprintf(stderr, "error: Cannot link module %s\n", import_file.c_str());
        std::abort();
      }
    }
  }

  void reset_app() {
    app_instance_.reset();
    imported_modules_.clear();
  }

  ws::Engine engine_;
  ws::Store store_{engine_};
  ws::Linker linker_{engine_};

  std::vector<fs::path> link_dirs_;
  std::optional<fs::path> workdir_;
  std::optional<ws::Instance> app_instance_;
  std::vector<ws::Module> imported_modules_;
};

Check how load_app works !! It works like runtime loader that loads properly all dependencies (shared libraries, but in case of wasi - wasm files) It accomplish it by reading imports with name of wasm module that should be loaded and searching the same file in search path. It makes working with wasmtime much much simpler for complex program that consists of many wasm files

alexcrichton commented 2 years ago

Ah so the wasmtime CLI does not do what you rexample application is doing, which is using the first level of the two-level imports to load modules from the filesystem. In general while that seems like it should work it tends to not work for most applications today since it requires everything to be coordinated in terms of linear memory otherwise. I think this might be a neat example to add still but I don't think it would make sense to add it natively to the header.

redradist commented 2 years ago

@alexcrichton

Ah so the wasmtime CLI does not do what you rexample application is doing, which is using the first level of the two-level imports to load modules from the filesystem. In general while that seems like it should work it tends to not work for most applications today since it requires everything to be coordinated in terms of linear memory otherwise. I think this might be a neat example to add still but I don't think it would make sense to add it natively to the header.

What I did is actually elf loader for wasmtime programs that could find libraries in other predefined places. I personally think it should be part of wasmtime to simplify working of big program that consists of multiple wasm files. I think comment regarding memory alignment should be the responsibility of user that will provide search path for wasm parts. Do you think it would be more productive to start disscussion regarding adding this functionality in main wamtime repo ?

alexcrichton commented 2 years ago

Yeah if you'd like to add this to Wasmtime itself it would be best to discuss over there.

I don't think this is really ready at this time though because wasm modules referring to each other by name and doing nontrivial things isn't really supported by toolchains today. That's sort of the "dynamic linking" story for wasm modules which currently is supported by Emscripten with JS glue but I don't believe anyone's worked on getting something working off the web.

redradist commented 1 year ago

@alexcrichton Lets see my example for WasiApp:

class WasiApp {
 public:
  explicit WasiApp(std::vector<fs::path> linkdirs,
                   std::optional<fs::path> workdir = std::nullopt)
      : link_dirs_{std::move(linkdirs)}
      , workdir_{std::move(workdir)} {
    linker_.define_wasi().unwrap();
  }

  void set_config(ws::WasiConfig&& config) {
    store_.context().set_wasi(std::move(config)).unwrap();
  }

  void load_app(const std::string& app_name) {
    reset_app();
    auto workdir = workdir_.value_or(fs::current_path());
    std::optional<fs::path> filename = find_file(workdir, app_name);
    ws::Module module = compile(filename.value());
    app_instance_ = link(app_name, module);
  }

  void run(const std::string& entry_point, const std::vector<ws::Val> &params) {
    ws::Func f = std::get<ws::Func>(*app_instance_.value().get(store_, entry_point));
    f.call(store_, {}).unwrap();
  }

 private:
  [[nodiscard]]
  static std::string read_wat_file(const char* name) {
    std::ifstream watFile;
    watFile.open(name);
    std::stringstream strStream;
    strStream << watFile.rdbuf();
    return strStream.str();
  }

  [[nodiscard]]
  static std::vector<uint8_t> read_wasm_file(const char* name) {
    std::ifstream bin_file(name, std::ios::binary);
    return {(std::istreambuf_iterator<char>(bin_file)), (std::istreambuf_iterator<char>())};
  }

  [[nodiscard]]
  static std::optional<fs::path> find_file(const fs::path& search_dir,
                                           const std::string& app_name) {
    std::optional<fs::path> filename;
    for (auto &p : fs::directory_iterator(search_dir)) {
      if (is_regular_file(p.path())) {
        if (p.path().stem() == app_name &&
            (p.path().extension() == ".wasm" ||
             p.path().extension() == ".wat")) {
          filename = p.path();
          break;
        }
      }
    }
    return filename;
  }

  [[nodiscard]]
  ws::Instance link(const std::string& name, const ws::Module& module) {
    std::unordered_map<std::string, std::vector<std::string>> imports;
    for (auto i : module.imports()) {
      imports[std::string(i.module())].push_back(std::string(i.name()));
    }
    for (auto& [import_file, import_symbols] : imports) {
      std::sort(import_symbols.begin(), import_symbols.end());
    }
    load_imports(imports);
    ws::Instance linking_instance = linker_.instantiate(store_, module).unwrap();
    linker_.define_instance(store_, name, linking_instance);
    imported_modules_.push_back(module);
    return linking_instance;
  }

  [[nodiscard]]
  ws::Module compile(const fs::path& path) {
    if (path.extension() == ".wat") {
      auto linking_wat = read_wat_file(path.string().c_str());
      return ws::Module::compile(engine_, linking_wat).unwrap();
    } else {
      auto linking_wasm = read_wasm_file(path.string().c_str());
      return ws::Module::compile(engine_, linking_wasm).unwrap();
    }
  }

  void load_imports(const std::unordered_map<std::string, std::vector<std::string>>& imports) {
    for (const auto& [import_file, import_symbols] : imports) {
      if (import_file.find("wasi_snapshot") != std::string::npos) {
        continue;
      }
      bool is_found_wasm = false;
      for (const auto& link_dirs : link_dirs_) {
        std::optional<fs::path> link_filename = find_file(link_dirs, import_file);
        if (link_filename) {
          ws::Module linking_module = compile(link_filename.value());
          std::vector<std::string> exports;
          for (const auto& e : linking_module.exports()) {
            exports.emplace_back(e.name());
          }
          std::sort(exports.begin(), exports.end());
          if (exports != import_symbols) {
            continue;
          }
          ws::Instance linking_instance = link(import_file, linking_module);
          is_found_wasm = true;
          break;
        }
      }
      if (!is_found_wasm) {
        fprintf(stderr, "error: Cannot link module %s\n", import_file.c_str());
        std::abort();
      }
    }
  }

  void reset_app() {
    app_instance_.reset();
    imported_modules_.clear();
  }

  ws::Engine engine_;
  ws::Store store_{engine_};
  ws::Linker linker_{engine_};

  std::vector<fs::path> link_dirs_;
  std::optional<fs::path> workdir_;
  std::optional<ws::Instance> app_instance_;
  std::vector<ws::Module> imported_modules_;
};

Check how load_app works !! It works like runtime loader that loads properly all dependencies (shared libraries, but in case of wasi - wasm files) It accomplish it by reading imports with name of wasm module that should be loaded and searching the same file in search path. It makes working with wasmtime much much simpler for complex program that consists of many wasm files

@alexcrichton If I will add this functionality, will you accept PR ?