Skip to content

Commit

Permalink
Common: Remove dependency on cpp-optparse
Browse files Browse the repository at this point in the history
Doesn't remove it from the full project since three tools still use it.

FEXLoader argument loader is fairly special since we have a generator
tied to it. This generator lets us have some niceties where everything
ends up being a string literal without any sort of dynamic requirements.

cpp-optparse has a systemic issue where it makes deep copies of
/everything/ all the time. This means it has huge runtime costs and huge
stack usage (4992 bytes). The new implementation uses 608 bytes of stack
space (plus 640 if it actually parses a strenum).

When profiling cpp-optparse with FEXLoader's argument it takes ~2,039,307 ns to parse the arguments!
As a direct comparison this new implementation takes ~56,885 ns.
Both sides not getting passed /any/ arguments, really shows how spicy
this library is.
  • Loading branch information
Sonicadvance1 committed Sep 15, 2024
1 parent e190d02 commit 4063b9d
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 101 deletions.
90 changes: 33 additions & 57 deletions FEXCore/Scripts/config_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,81 +351,57 @@ def print_config_option(type, group_name, json_name, default_value, short, choic

output_argloader.write("\n");

def print_argloader_options(options):
output_argloader.write("#ifdef BEFORE_PARSE\n")
output_argloader.write("#undef BEFORE_PARSE\n")
def print_argloader_options_map(options):
output_argloader.write("#ifdef ARG_TO_CONFIG\n")
output_argloader.write("#undef ARG_TO_CONFIG\n")
for op_group, group_vals in options.items():
for op_key, op_vals in group_vals.items():
default = op_vals["Default"]

if (op_vals["Type"] == "str" or op_vals["Type"] == "strarray" or op_vals["Type"] == "strenum"):
# Wrap the string argument in quotes
default = "\"" + default + "\""
value_type = op_vals["Type"]

# Textual default rather than enum based
if ("TextDefault" in op_vals):
default = "\"" + op_vals["TextDefault"] + "\""
ParserType = "ParserTypes::String"

short = None
choices = None
if value_type == "bool":
ParserType = "ParserTypes::BoolTrue"

if value_type == "strenum":
ParserType = "ParserTypes::StringEnum"

if value_type == "strarray":
ParserType = "ParserTypes::StringArray"

short = None
if ("ShortArg" in op_vals):
short = op_vals["ShortArg"]
if ("Choices" in op_vals):
choices = op_vals["Choices"]

print_config_option(
op_vals["Type"],
op_group,
op_key,
default,
short,
choices,
op_vals["Desc"])
if short != None:
output_argloader.write("{{\"-{}\", FEXCore::Config::ConfigOption::CONFIG_{}, {}}},\n".format(short, op_key.upper(), ParserType))

output_argloader.write("\n")
output_argloader.write("{{\"--{}\", FEXCore::Config::ConfigOption::CONFIG_{}, {}}},\n".format(op_key.lower(), op_key.upper(), ParserType))

if value_type == "bool":
output_argloader.write("{{\"--no-{}\", FEXCore::Config::ConfigOption::CONFIG_{}, ParserTypes::BoolFalse}},\n".format(op_key, op_key.upper()))
output_argloader.write("#endif\n")

def print_parse_argloader_options(options):
output_argloader.write("#ifdef AFTER_PARSE\n")
output_argloader.write("#undef AFTER_PARSE\n")
def print_parse_argloader_enumparser_new(options):
output_argloader.write("#ifdef STR_ENUM_PARSE\n")
output_argloader.write("#undef STR_ENUM_PARSE\n")
for op_group, group_vals in options.items():
for op_key, op_vals in group_vals.items():
output_argloader.write("if (Options.is_set_by_user(\"{0}\")) {{\n".format(op_key))

value_type = op_vals["Type"]
NeedsString = False
conversion_func = "fextl::fmt::format(\"{}\", "
if ("ArgumentHandler" in op_vals):
NeedsString = True
conversion_func = "FEXCore::Config::Handler::{0}(".format(op_vals["ArgumentHandler"])
if (value_type == "str"):
NeedsString = True
conversion_func = "std::move("
if (value_type == "bool"):
# boolean values need a decimal specifier. Otherwise fmt prints strings.
conversion_func = "fextl::fmt::format(\"{:d}\", "

if (value_type == "strenum"):
output_argloader.write("\tfextl::string UserValue = Options[\"{0}\"];\n".format(op_key))
output_argloader.write("\tSet(FEXCore::Config::ConfigOption::CONFIG_{}, FEXCore::Config::EnumParser<FEXCore::Config::{}ConfigPair>(FEXCore::Config::{}_EnumPairs, UserValue));\n".format(op_key.upper(), op_key, op_key, op_key))
elif (value_type == "strarray"):
# these need a bit more help
output_argloader.write("\tauto Array = Options.all(\"{0}\");\n".format(op_key))
output_argloader.write("\tfor (auto iter = Array.begin(); iter != Array.end(); ++iter) {\n")
output_argloader.write("\t\tSet(FEXCore::Config::ConfigOption::CONFIG_{0}, *iter);\n".format(op_key.upper()))
output_argloader.write("\t}\n")
else:
if (NeedsString):
output_argloader.write("\tfextl::string UserValue = Options[\"{0}\"];\n".format(op_key))
else:
output_argloader.write("\t{0} UserValue = Options.get(\"{1}\");\n".format(value_type, op_key))
output_argloader.write("case FEXCore::Config::ConfigOption::CONFIG_{}: {{\n".format(op_key.upper()))
output_argloader.write("\tauto Converted = FEXCore::Config::EnumParser<FEXCore::Config::{}ConfigPair>(FEXCore::Config::{}_EnumPairs, SecondArg);\n".format(op_key, op_key, op_key))
output_argloader.write("\tif (Converted) { SecondArg = *Converted; }\n")
output_argloader.write("\tfextl::fmt::print(\"enum:{}\\n\", SecondArg);\n")

output_argloader.write("\tSet(FEXCore::Config::ConfigOption::CONFIG_{0}, {1}UserValue));\n".format(op_key.upper(), conversion_func))
output_argloader.write("}\n")
output_argloader.write("\tLoader->SetArg(FEXCore::Config::ConfigOption::CONFIG_{}, SecondArg);\n".format(op_key.upper()))
output_argloader.write("break;\n".format(op_key.upper()))
output_argloader.write("}\n")

output_argloader.write("#endif\n")

output_argloader.write("#endif\n")

def print_parse_envloader_options(options):
output_argloader.write("#ifdef ENVLOADER\n")
Expand Down Expand Up @@ -574,8 +550,8 @@ def check_for_duplicate_options(options):

# Generate argument loader code
output_argloader = open(output_argumentloader_filename, "w")
print_argloader_options(options);
print_parse_argloader_options(options);
print_argloader_options_map(options);
print_parse_argloader_enumparser_new(options);

# Generate environment loader code
print_parse_envloader_options(options);
Expand Down
255 changes: 224 additions & 31 deletions Source/Common/ArgumentLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,240 @@
#include <FEXCore/fextl/string.h>
#include <FEXCore/fextl/vector.h>

#include "cpp-optparse/OptionParser.h"
#include "git_version.h"

#include <stdint.h>

namespace FEX::ArgLoader {
void FEX::ArgLoader::ArgLoader::Load() {
RemainingArgs.clear();
ProgramArguments.clear();
if (Type == LoadType::WITHOUT_FEXLOADER_PARSER) {
LoadWithoutArguments();
return;
}
enum class ParserTypes : uint32_t {
String,
BoolTrue,
BoolFalse,
StringEnum,
StringArray,
Max,
};

optparse::OptionParser Parser {};
Parser.version("FEX-Emu (" GIT_DESCRIBE_STRING ") ");
optparse::OptionGroup CPUGroup(Parser, "CPU Core options");
optparse::OptionGroup EmulationGroup(Parser, "Emulation options");
optparse::OptionGroup DebugGroup(Parser, "Debug options");
optparse::OptionGroup HacksGroup(Parser, "Hacks options");
optparse::OptionGroup MiscGroup(Parser, "Miscellaneous options");
optparse::OptionGroup LoggingGroup(Parser, "Logging options");
struct OptionDetails {
std::string_view Arg;
FEXCore::Config::ConfigOption Option;
ParserTypes ParseType;
};

#define BEFORE_PARSE
constexpr static OptionDetails ArgToOption[] = {
#define ARG_TO_CONFIG
#include <FEXCore/Config/ConfigOptions.inl>
};

const OptionDetails* FindOption(std::string_view Argument) {
for (auto& Arg : ArgToOption) {
if (Arg.Arg == Argument) {
return &Arg;
}
}

return nullptr;
}

void PrintHelp() {
const char* Arguments[3] {};
Arguments[0] = "man";
Arguments[1] = "FEX";
Arguments[2] = nullptr;

execvp("man", (char* const*)Arguments);
}

void ExitWithError(std::string_view Error) {
fextl::fmt::print("Error: {}\n", Error);
std::exit(1);
}

class ArgParser final {
public:
ArgParser(FEX::ArgLoader::ArgLoader* Loader)
: Loader {Loader} {}

void Parse(int argc, char** argv);
void Version(std::string_view version) {
_Version = version;
}

fextl::vector<fextl::string> GetGuestArgs() {
return std::move(RemainingArgs);
}
fextl::vector<const char*> GetParsedArgs() {
return std::move(ProgramArguments);
}

Parser.add_option_group(CPUGroup);
Parser.add_option_group(EmulationGroup);
Parser.add_option_group(DebugGroup);
Parser.add_option_group(HacksGroup);
Parser.add_option_group(MiscGroup);
Parser.add_option_group(LoggingGroup);
private:
FEX::ArgLoader::ArgLoader* Loader;
std::string_view _Version {};
fextl::vector<fextl::string> RemainingArgs {};
fextl::vector<const char*> ProgramArguments {};
using ParseArgHandler = void (ArgParser::*)(std::string_view Arg, std::string_view SecondArg, const OptionDetails* Details);

void ParseArgument_String(std::string_view Arg, std::string_view SecondArg, const OptionDetails* Details) {
Loader->SetArg(Details->Option, SecondArg);
}

void ParseArgument_BoolTrue(std::string_view Arg, std::string_view SecondArg, const OptionDetails* Details) {
using namespace std::literals;
Loader->SetArg(Details->Option, "1"sv);
}

optparse::Values Options = Parser.parse_args(argc, argv);
void ParseArgument_BoolFalse(std::string_view Arg, std::string_view SecondArg, const OptionDetails* Details) {
using namespace std::literals;
Loader->SetArg(Details->Option, "0"sv);
}

using int32 = int32_t;
using uint32 = uint32_t;
#define AFTER_PARSE
void ParseArgument_StringEnum(std::string_view Arg, std::string_view SecondArg, const OptionDetails* Details) {
switch (Details->Option) {
#define STR_ENUM_PARSE
#include <FEXCore/Config/ConfigOptions.inl>
RemainingArgs = Parser.args();
ProgramArguments = Parser.parsed_args();
[[unlikely]] default:
ExitWithError(fextl::fmt::format("Unknown strenum argument: {}", Arg));
break;
}
}

constexpr static std::array<ParseArgHandler, FEXCore::ToUnderlying(ParserTypes::Max)> Parser = {
&ArgParser::ParseArgument_String,
&ArgParser::ParseArgument_BoolTrue,
&ArgParser::ParseArgument_BoolFalse,
&ArgParser::ParseArgument_StringEnum,
// Behaves the same as string but appends multiple.
&ArgParser::ParseArgument_String,
};

void ParseArgument(std::string_view Arg, std::string_view SecondArg, const OptionDetails* Details) {
std::invoke(Parser[FEXCore::ToUnderlying(Details->ParseType)], this, Arg, SecondArg, Details);
}
};

bool NeedsArg(const OptionDetails* Details) {
return Details->ParseType == ParserTypes::String || Details->ParseType == ParserTypes::StringEnum || Details->ParseType == ParserTypes::StringArray;
}

void ArgParser::Parse(int argc, char** argv) {
// Skip argv[0]
int ArgParsed = 1;
for (; ArgParsed < argc; ++ArgParsed) {
std::string_view Arg = argv[ArgParsed];

// Special case version and help
if (Arg == "--version") [[unlikely]] {
fextl::fmt::print("{}\n", _Version);
std::exit(0);
}

if (Arg == "-h" || Arg == "--help") [[unlikely]] {
PrintHelp();
std::exit(0);
}

if (Arg == "--") {
// Special case break. Remaining arguments get passed to guest.
++ArgParsed;
break;
}

const bool IsShort = Arg.find("--", 0, 2) == Arg.npos && Arg.find("-", 0, 1) == 0;
const bool IsLong = Arg.find("--", 0, 2) == 0;

std::string_view ArgFirst {};
std::string_view ArgSecond {};

const OptionDetails* OptionDetails {};

if (IsShort) {
ArgFirst = Arg;

OptionDetails = FindOption(ArgFirst);

if (OptionDetails == nullptr) [[unlikely]] {
ExitWithError(fextl::fmt::format("Unsupported argument: {}", Arg));
}

if (NeedsArg(OptionDetails)) {
++ArgParsed;
ArgSecond = argv[ArgParsed];
}
} else if (IsLong) {
const auto Split = Arg.find_first_of('=');
bool NeedsSplitArg {};
if (Split == Arg.npos) {
ArgFirst = Arg;
OptionDetails = FindOption(Arg);

if (OptionDetails == nullptr) [[unlikely]] {
ExitWithError(fextl::fmt::format("Unsupported argument: {}", Arg));
}

NeedsSplitArg = NeedsArg(OptionDetails);
} else {
ArgFirst = Arg.substr(0, Split);
OptionDetails = FindOption(ArgFirst);

if (OptionDetails == nullptr) [[unlikely]] {
ExitWithError(fextl::fmt::format("Unsupported argument: {}", Arg));
}

NeedsSplitArg = NeedsArg(OptionDetails);
}

if (NeedsSplitArg && Split == Arg.npos) [[unlikely]] {
ExitWithError(fextl::fmt::format("{} needs argument", Arg));
}

if (!NeedsSplitArg && Split != Arg.npos) [[unlikely]] {
ExitWithError(fextl::fmt::format("{} can't have argument", Arg));
}

if (NeedsSplitArg) {
ArgSecond = Arg.substr(Split + 1, Arg.size());

if (ArgSecond.empty()) [[unlikely]] {
ExitWithError(fextl::fmt::format("{} needs argument", Arg));
}
}
}

if (ProgramArguments.empty() && OptionDetails == nullptr) {
// In the special case that we hit a parse error and we haven't parsed any arguments, pass everything.
// This handles the typical case eg: `FEXLoader /usr/bin/ls /`.
// Some would claim that `--` should be used to split FEX arguments from sub-application arguments.
break;
}

// Unsupported FEX argument. Error.
if (ProgramArguments.empty() && OptionDetails == nullptr) [[unlikely]] {
ExitWithError(fextl::fmt::format("Unsupported argument: {}", Arg));
}

// Save the FEX argument.
ProgramArguments.emplace_back(argv[ArgParsed]);

// Now parse the argument.
ParseArgument(Arg, ArgSecond, OptionDetails);
}

// Pass any remaining arguments to guest application
for (; ArgParsed < argc; ++ArgParsed) {
RemainingArgs.emplace_back(argv[ArgParsed]);
}
}

void FEX::ArgLoader::ArgLoader::Load() {
RemainingArgs.clear();
ProgramArguments.clear();
ArgParser Parser(this);
Parser.Version("FEX-Emu (" GIT_DESCRIBE_STRING ") ");
Parser.Parse(argc, argv);
RemainingArgs = Parser.GetGuestArgs();
ProgramArguments = Parser.GetParsedArgs();
}

void FEX::ArgLoader::ArgLoader::SetArg(FEXCore::Config::ConfigOption Option, std::string_view Arg) {
Set(Option, Arg);
}

void FEX::ArgLoader::ArgLoader::LoadWithoutArguments() {
Expand Down
Loading

0 comments on commit 4063b9d

Please sign in to comment.