microsoft / microsoft/snmalloc

fallible memory allocation

Open
#423 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
C++
Stars
2k
Forks
138
Avg merge
11h 19m
Merged PRs (30d)
5

Description

Client language like rust may want to provide fallible memory allocation APIs.
https://rust-lang.github.io/rfcs/2116-alloc-me-maybe.html

It is of course not a required feature. The std library states that Alloc implementation is allowed to be implemented on top of a native allocator that aborts on allocation failure. However, it can be good is consider support fallible allocation in snmalloc.

#pragma once
#include <variant>
namespace snmalloc
{
  using Unit = std::monostate;
  enum class ErrorCode : size_t
  {
    Uninitialised = 0,
    OutOfMemory = 1,
    InternalLogicError = 2,
    InvalidArgument = 3,
    OsFailure = 4
  };

  struct Error
  {
    ErrorCode err_code;
    char const* description;
  };

  template<typename T>
  class Result
  {
    using ImplType = std::variant<Error, T>;
    std::variant<Error, T> impl; // 0 => failed, 1 => success

    static constexpr size_t SUCCESS = 1;
    static constexpr size_t FAILURE = 0;
    static constexpr bool EXCEPTION_STATE =
      noexcept(std::construct_at(std::declval<T*>(), std::declval<T&&>()));

    constexpr Result(std::variant<Error, T> x) : impl(std::move(x)) {}

  public:
    Result(const Result&) = default;
    Result(Result&&) noexcept(EXCEPTION_STATE) = default;
    static constexpr Result<T> uninitialised()
    {
      return ImplType(Error{ErrorCode::Uninitialised, "uninitialised result"});
    }

    template<typename... Args>
    static constexpr Result<T> success(Args&&... args)
    {
      return ImplType(T{std::forward<Args>(args)...});
    }

    static constexpr Result<T>
    failure(ErrorCode err_code, char const* description)
    {
      return ImplType(Error{err_code, description});
    }

    T unsafe_unwrap()
    {
      auto storage = uninitialised();
      impl.swap(storage.impl);
      auto* pointer = std::get_if<T>(&storage.impl);
      SNMALLOC_ASSUME(pointer);
      return std::move(*pointer);
    }

    T unwrap()
    {
      auto storage = uninitialised();
      impl.swap(storage.impl);
      if (auto* pointer = std::get_if<T>(&storage.impl))
      {
        return std::move(*pointer);
      }
      snmalloc::error("unwrap value from a failed result");
    }

    Error unsafe_error()
    {
      auto* pointer = std::get_if<Error>(&impl);
      SNMALLOC_ASSUME(pointer);
      return *pointer;
    }

    Error error()
    {
      if (auto* pointer = std::get_if<Error>(&impl))
      {
        return *pointer;
      }
      snmalloc::error("expecting error from a successful result");
    }

    T& unsafe_ref()
    {
      auto* pointer = std::get_if<T>(&impl);
      SNMALLOC_ASSUME(pointer);
      return *pointer;
    }

    T& ref()
    {
      if (auto* pointer = std::get_if<T>(&impl))
      {
        return *pointer;
      }
      snmalloc::error("referencing value from a failed result");
    }

    T const& unsafe_ref() const
    {
      auto const* pointer = std::get_if<T>(&impl);
      SNMALLOC_ASSUME(pointer);
      return *pointer;
    }

    T const& ref() const
    {
      if (auto const* pointer = std::get_if<T>(&impl))
      {
        return *pointer;
      }
      snmalloc::error("referencing value from a failed result");
    }

    template<class F>
    auto map(F func)
    {
      using TargetType = decltype(func(unsafe_unwrap()));
      if (std::get_if<T>(&impl))
      {
        return Result<TargetType>{func(unsafe_unwrap())};
      }
      // without assume, GCC will generate ud2
      // another option is to use std::visit, but its codegen scares me.
      auto err = std::get_if<Error>(&impl);
      SNMALLOC_ASSUME(err);
      return Result<TargetType>{*err};
    }

    template<class F>
    auto flat_map(F func)
    {
      using TargetType = decltype(func(unsafe_unwrap()));
      if (std::get_if<T>(&impl))
      {
        return func(unsafe_unwrap());
      }
      // without assume, GCC will generate ud2
      // another option is to use std::visit, but its codegen scares me.
      auto err = std::get_if<Error>(&impl);
      SNMALLOC_ASSUME(err);
      return TargetType{*err};
    }

    bool is_success() const
    {
      return SUCCESS == impl.index();
    }

    bool is_failure() const
    {
      return FAILURE == impl.index();
    }
  };

} // namespace snmalloc

I am now prototyping this with the structure above. We can provide sth like the following on par:

  template<typename PAL>
  concept ConceptPAL_get_entropy64 = requires()
  {
    { PAL::get_entropy64() } -> ConceptSame<uint64_t>;
    { PAL::get_entropy64_fallible() } -> ConceptSame<Result<uint64_t>>;
  };

But it may require a big refactor across the project to finally support the feature.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No project files or tests are named. Start by reviewing the proposed Result structure and the ConceptPAL_get_entropy64 interface in the issue, then trace how allocator and PAL operations currently report failure. Done means defining an agreed fallible-allocation design and applying it consistently across the project.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, rust
Domain
operating-systems
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.