Skip to main content

Crate compatdb_core

Crate compatdb_core 

Source
Expand description

The per-process rules of this Wine’s compatdb.so, and the wire format a launcher hands them over in.

compatdb.so is the unix library Wine’s ntdll loads into every process of the bundle before any PE code runs. It reads the process’s image name and version resource, takes the built-in rules of builtin, appends the rules a launcher passed in the environment variable ENV_VAR (WINE_COMPATDB), folds every rule that matches the process into one Resolution and applies it: the Direct3D trees to load, the renderer of wined3d, which OpenGL the Mac driver gives, DLL load-order overrides, the DPI awareness, large address awareness, whether the Mac driver reads raw mouse input from GameController, whether no-execute stays on, whether the Metal performance HUD shows, command-line arguments and environment entries. It logs every value with the rule it came from: the lines on Wine’s stderr that start with compatdb:, which docs/compatdb.md describes, show a launcher author what a process got and why.

This crate is the only place that knows the rules and their format. A launcher uses it to build its rules (Rule, Table), to write the value of ENV_VAR (Table’s Display) and to show what a program would get (Table::preview). compatdb.so uses the same crate to read the value (Table::parse_lenient) and resolve it (Table::resolve); the runtime module holds what only it needs.

Everything here is pure: it works on &str, &[u16] (UTF-16 as Wine hands it over) and owned values, with no unsafe and no I/O, so the decision logic is unit-tested on the host and the .so itself only has to move bytes in and out of the process.

§Quick start

A launcher builds one rule for every process from its launch-wide settings and one per program it knows something about, writes the table and starts Wine with it in ENV_VAR. The whole process tree inherits the variable, so a rule for a program the game starts reaches it too.

The settings layer fills a rule from text without the launcher knowing any key: Key::from_setting names the key, Key::kind says whether it holds one value, a list or environment entries, and Rule::set, Rule::set_list or Rule::set_env take the value. Every error is a FieldError to print as it is, and FieldError::severity says whether to warn and go on or to refuse the settings.

use std::process::Command;

use compatdb_core::{
    Argument, Dxgi, ENV_VAR, Exe, Fingerprint, Key, Rule, Severity, Table, VersionInfo,
};

// The launch-wide settings, as the launcher's own config holds them.
let mut every = Rule::new(Exe::Any);
for (name, value) in [("dpi_aware", "false"), ("GCMouse", "Default")] {
    every.set(Key::from_setting(name)?, value)?;
}

// A rule for one game, from typed parts.
let game = Rule::new(Exe::new("Game.exe")?)
    .with_company(Fingerprint::new("Some Vendor")?)
    .with_dxgi(Dxgi::Wined3d)
    .with_argument(Argument::new("-windowed")?);

// A retired value is a warning: the rule stays as it was.
let mut old = Rule::new(Exe::new("Old.exe")?);
let error = old.set(Key::Dxgi, "gptk").err().ok_or("gptk was taken")?;
assert_eq!(error.severity(), Severity::Warning);
assert!(old.sets_nothing());

let mut table = Table::new();
table.push(every);
table.push(game);
let value = table.to_string();
assert_eq!(
    value,
    "exe=*;dpi_aware=false;gcmouse=default\n\
     exe=Game.exe;company=Some Vendor;dxgi=wined3d;arguments=-windowed"
);
let mut wine = Command::new("bin/wine");
wine.env(ENV_VAR, &value);

// What that game would get, the built-in rules included.
let version = VersionInfo {
    company: "Some Vendor Ltd.".into(),
    ..VersionInfo::default()
};
let resolution = table.preview("Game.exe", &version);
assert_eq!(resolution.dxgi(), Some(Dxgi::Wined3d));
assert_eq!(resolution.dpi_aware(), Some(false));
// `default` hands gcmouse back to upstream Wine.
assert_eq!(resolution.gcmouse(), None);
assert_eq!(resolution.arguments, ["-windowed"]);

§Format

The value of ENV_VAR is one record per line, each a rule:

exe=Game.exe;company=Some Vendor;d3d9=wined3d;dpi_aware=false;env=VKD3D_CONFIG=a%3Bb
exe=OldGame.exe;large_address_aware=true
exe=*;gcmouse=false
  • A line ends at LF, and a CR before the LF is dropped. A line of whitespace alone is skipped. Lines are counted from 1, blank lines included; that number is the rule’s Source::Line, and the line of each Diagnostic.
  • A record is fields joined by ;. An empty field is skipped.
  • A field is key=value, and the key ends at the first =. The keys are those of Key.
  • Every record needs an exe. The other matchers, company, product and original_filename, are optional (see Matching). A matcher or a scalar setting holds one value, and a list takes one entry per field, in order.
  • A scalar setting takes one of the spellings of Key::values: one of its words, or default, which resets it (Setting::Reset).
  • arguments_override with an empty value sets the override without adding an entry, so arguments_override= alone clears the arguments. An empty entry of arguments or debug_channels is refused.
  • An env field is env=NAME=value. The name ends at the first =, so a name cannot hold one (EnvName); the value may. An empty value, or none, removes the variable.
  • What each list entry and matcher may hold is the type of its part: Exe, Fingerprint, DllOverride (needs a =), Argument, DebugChannel (no ,) and EnvName. None of them holds a NUL.

Escaping. In a value, Display writes %, ; and every ASCII control character (U+0000 to U+001F and U+007F, CR and LF among them) as %XX, the uppercase hex of the byte. Everything else, =, ,, spaces and non-ASCII included, is written as it is, in UTF-8. Reading decodes every %XX, in either case, to its byte, and the bytes of a value must be UTF-8 without a NUL: %C3%A9 is é, and %FF or %00 drops the field. A % that two hex digits do not follow is kept as it is, with a diagnostic.

The case rule. Every word the format defines is read ASCII-case-insensitively and written in lowercase: the keys, the words of the scalar settings, true, false, default, and the v of a legacy header. User data is kept byte for byte: the exe, the fingerprint values, list entries and env names and values. So DXGI=WineD3D reads as dxgi=wined3d, while exe=Game.EXE keeps its case (matching ignores it, see Matching).

There is no format version. A first line of v= and ASCII digits, the header launchers wrote for an older format, is skipped without a diagnostic; it still counts as a line. Compatibility is per field instead: compatdb.so ignores a field whose key it does not know or whose value its key does not take, with a warning, and applies the rest of the record. A key added later is therefore only ever a setting: the matcher keys are a closed set, since a library that ignored an unknown matcher would apply the rule to more processes than it names. The name and enabled keys of the older format, where a line changed or dropped a built-in rule by its name, are unknown keys like any other, and a compatdb.so from before that change drops every line without name, so a table in this format does nothing on an older bundle.

Display writes the canonical text: one line per rule and no line break after the last, the fields in the order of Key::ALL, every word in lowercase, and none of the built-in rules, which compatdb.so has compiled in.

§Strict and lenient parsing

Table::parse_lenient reads the value the way compatdb.so does, which must never abort a process. Each problem is a Diagnostic, whose Display is the warning the library logs:

  • A field with an unknown key, a value its key or its part does not take, no =, escapes that are not UTF-8, or a NUL is left out, and the rest of its line still applies.
  • A line without an exe is left out as a whole, since it matches no process.
  • A second field for a matcher or a scalar setting replaces the first.
  • A % that two hex digits do not follow is kept as it is.

Table::parse_strict, which is also Table’s FromStr, refuses every problem lenient parsing reports, with a ParseError that lists them. A launcher that writes the value with Display never needs to read it; strict parsing is for text a person wrote.

Two guarantees hold, and the round-trip suite checks them on generated tables: a table built with Table::new and Table::push reads back from its text as itself, and canonical text writes back byte for byte.

use compatdb_core::Table;

let table: Table = "EXE=Game.exe;DPI_Aware=False;env=VKD3D_CONFIG=a%3Bb".parse()?;
let text = table.to_string();
assert_eq!(text, "exe=Game.exe;dpi_aware=false;env=VKD3D_CONFIG=a%3Bb");
assert_eq!(Table::parse_strict(&text)?, table);
assert_eq!(Table::parse_strict(&text)?.to_string(), text);

// Lenient parsing keeps what it can read and says what it left out.
let (table, diagnostics) = Table::parse_lenient("exe=Game.exe;bogus=1;gcmouse=yes");
assert_eq!(table.rules().len(), 1);
let warnings: Vec<String> = diagnostics.iter().map(ToString::to_string).collect();
assert_eq!(
    warnings,
    [
        "WINE_COMPATDB line 1: ignoring unknown key \"bogus\"",
        "WINE_COMPATDB line 1: ignoring unknown gcmouse value \"yes\"",
    ]
);
assert!(Table::parse_strict("exe=Game.exe;bogus=1").is_err());

§Matching

A rule has no name: it is its matchers and its settings (Rule::matches).

  • exe matches the image basename ASCII-case-insensitively, or every process when it is * (Exe::Any).
  • company, product and original_filename are optional ASCII-case-insensitive substrings of the image’s version strings CompanyName, ProductName and OriginalFilename (VersionInfo). A string the image lacks reads as empty, so a rule that pins a value for it does not match.
  • A rule whose exe matches but whose fingerprint does not does not apply; nothing falls back to it, and the other rules that match still apply.
  • Table::duplicate_matchers finds rules that match exactly the same processes, which in a hand-written table is a mistake.

§Folding

Every rule that matches a process applies; no rule removes or replaces another. Table::resolve folds them in this order, and within each group a rule without a fingerprint before one with, otherwise in table order (line order for ENV_VAR):

  1. the built-in * rules;
  2. the * rules of ENV_VAR;
  3. the built-in rules that name an executable;
  4. the rules of ENV_VAR that name an executable.

So a launch-wide * rule replaces a built-in default but not what a built-in rule for one program sets: that fix survives launch-wide settings, and a rule of the launcher for that program changes it, with or without a fingerprint. Within ENV_VAR, a rule for an executable beats a * rule whatever their line order.

  • A scalar setting ends up with the value of the last rule that sets it. default resets it: from that rule on it is unset, as if no rule had set it, so upstream Wine decides unless a rule folded later sets it again. For most settings that is the same as one of the values, but not for dpi_aware: false makes a program unaware, default leaves it to the registry and the manifest.
  • The lists accumulate in fold order.
  • arguments_override replaces the arguments the rules folded before it gathered, the built-in ones included, with its own entries; the rule’s own arguments and those of the rules folded after it still append.
  • A dll_overrides entry with the order default, such as mscoree,mshtml=default, takes its modules out of the entries the rules folded before it gave them and adds none, so the registry and WINEDLLOVERRIDES decide for them again.
  • The env entries of a built-in * rule without a fingerprint are defaults: one sets its variable only where the process does not have it, so a value whoever started the process exported wins, an empty one included. Every other entry, a * rule’s of ENV_VAR included, replaces the variable, and an entry without a value removes it. Resolution::env_steps says what each entry does in a process.
  • wined3d and metal_hud become env entries too (Resolution::fold_derived_env): wined3d the WINE_D3D_CONFIG after every rule’s own entries, metal_hud the MTL_HUD_ variables where the rule that set it folds, before its own entries, so an env entry for one of them from that rule or a later one wins.
  • A program inherits the environment of the process that started it, the values compatdb.so set there included. A value a rule for one program sets for a variable with a * default therefore carries into the programs it starts, whose default finds the variable set. A value it removes does not: the default sets it again in the child. The settings ntdll stores for the process (dpi_aware, large_address_aware, gcmouse, force_nx, opengl) are its own: a child gets the value of its own rules.

Resolution keeps where each value came from (Origin, ScalarState), which compatdb.so logs.

use compatdb_core::{Dxgi, Exe, Origin, Rule, Scalar, Table, VersionInfo};

let mut table = Table::new();
table.push(Rule::new(Exe::Any).with_dxgi(Dxgi::Wined3d));

// A game gets the launch-wide value over the built-in default.
let game = table.preview("Game.exe", &VersionInfo::default());
assert_eq!(game.dxgi(), Some(Dxgi::Wined3d));
assert_eq!(game.scalar(Scalar::Dxgi).set_by, Some(Origin::Line(1)));

// Steam's web helper keeps the DXMT its built-in rule pins.
let helper = VersionInfo {
    company: "Valve Corporation".into(),
    product: "Steam Client WebHelper".into(),
    ..VersionInfo::default()
};
let steam = table.preview("steamwebhelper.exe", &helper);
assert_eq!(steam.dxgi(), Some(Dxgi::Dxmt));
assert!(matches!(
    steam.scalar(Scalar::Dxgi).set_by,
    Some(Origin::BuiltInFor(_))
));

// A rule of the launcher for the helper changes it.
table.push(Rule::new(Exe::new("steamwebhelper.exe")?).with_dxgi(Dxgi::Wined3d));
let steam = table.preview("steamwebhelper.exe", &helper);
assert_eq!(steam.dxgi(), Some(Dxgi::Wined3d));

§Keys

Key lists every field. Each variant says what it does and what a process gets without it, and links to the getter of Rule with the details: when it applies, which part of Wine reads it and which built-in rule sets it.

Key::kind says which of these a key is, Key::values gives the spellings of a scalar setting, default included, Key::label its name for a person, and Key::upstream what upstream Wine does without it, as compatdb.so logs it. For a launcher’s templates and help, Key::summary says in one line what a key does, Key::example gives a value to show and Key::docs_url links to its doc; DocSection::url links to a section of this page.

§Built-in rules

builtin holds the rules compiled into compatdb.so, one function each, whose doc says why the rule exists. builtin::defaults match every process: a behaviour of this Wine that differs from upstream Wine and that a rule can switch is off in the code, and one of these turns it on, so the log of a process shows its whole configuration with the source of every value, and a launcher rule that sets the same key changes it. builtin::programs each name an executable and pin its version resource, so none of them catches an unrelated program of the same name; a test enforces this.

§Recipes

Each is one line of ENV_VAR; Game.exe stands for the game’s executable. Rule::set with the same keys and values builds them.

GoalRule
Re-enable Wine’s Mono for every processexe=*;dll_overrides=mscoree=b
Prefer a native .NETexe=*;dll_overrides=mscoree=n,b
Hand Mono and Gecko back to the registry and WINEDLLOVERRIDESexe=*;dll_overrides=mscoree,mshtml=default
Make one game DPI-unawareexe=Game.exe;dpi_aware=false
Let the registry and the manifest decide DPI awarenessexe=*;dpi_aware=default
Give one 32-bit game 4 GBexe=Game.exe;large_address_aware=true
Give one game the cursor’s mouse movementexe=Game.exe;gcmouse=false
Give every process wined3d for D3D10 and D3D11, and no D3D12exe=*;dxgi=wined3d
Give every process wined3d for D3D9exe=*;d3d9=wined3d
Hide D3D12 from one game, keeping DXMT for D3D10 and D3D11exe=Game.exe;dll_overrides=d3d12,d3d12core=
Change a vkd3d-proton setting for one gameexe=Game.exe;env=VKD3D_CONFIG=<options>
Give one game Wine’s no-execute behaviourexe=Game.exe;force_nx=false
Give one game FEX’s full x87 precisionexe=Game.exe;env=FEX_X87REDUCEDPRECISION=
Give one game Apple’s OpenGLexe=Game.exe;opengl=cgl
Put wined3d on Vulkan for one gameexe=Game.exe;wined3d=vulkan
Show the Metal HUD in one gameexe=Game.exe;metal_hud=true
Leave one game to upstream Wine for a settingexe=Game.exe;<key>=default
Replace the arguments the rules give one gameexe=Game.exe;arguments_override=-windowed
Give one game no arguments from any ruleexe=Game.exe;arguments_override=
Change what a built-in rule sets for one programexe=Launcher.exe;dxgi=dxmt

Most of them with exe=* do the same for every process, except where a built-in rule for one program sets the key. Rule::set refuses arguments_override in a * rule: for every process it would take away the arguments the built-in rules give single programs.

Hiding D3D12 disables both of vkd3d-proton’s modules, so d3d12.dll fails to load and a game that can use D3D11 falls back to it on DXMT; a game that only has D3D12 does not start with it, nor with dxgi=wined3d.

§Launcher responsibilities

  • Write the value with Table’s Display and nothing else. Never write the format by hand, add a version header or send the built-in rules: Display leaves them out, since compatdb.so has them.
  • Remove the variables of inherited_conflicts from the environment Wine starts with when the launcher inherited them, so a launch does not depend on where the launcher was started from.
  • Build WINEDEBUG itself: Wine reads it before compatdb.so loads. Its own entries come first, then Table::winedebug, joined with ,.
  • Ask the crate instead of naming keys: Key::replaces_on_merge when it lays one layer of its configuration over another, and Key::allowed_on_any for the settings it offers for every process.
  • Refuse an environment entry for a variable a setting replaced (retired_env); Rule::set_env does, and check_env_name checks a name on its own. That includes WINEDLLOVERRIDES: an override compatdb.so adds replaces its entry for the same module, so it cannot undo one, while a rule’s <module>=default can.
  • Show or hide the Metal HUD with Key::MetalHud, not with its own MTL_HUD_ entries in env.
  • Put env entries and metal_hud on the rule of the program that needs them, not on a program that starts games, such as a store client: the games it starts inherit them.
  • Name a former built-in rule with builtin::former, which says what to do instead; such a name reaches nothing any more.
  • Expect a changed table to reach the processes that outlive one launch, such as explorer.exe and services.exe, only in the next wineserver session: they keep the rules of the launch that started it.
use compatdb_core::{Exe, FieldError, Key, Rule, Table, inherited_conflicts, retired_env};

let mut game = Rule::new(Exe::new("Game.exe")?);
game.set(Key::DebugChannels, "+relay,-seh")?;
let mut table = Table::new();
table.push(game);
let mut winedebug = vec![String::from("-all")];
winedebug.extend(table.winedebug());
assert_eq!(winedebug.join(","), "-all,Game.exe:+relay,Game.exe:-seh");

let mut every = Rule::new(Exe::Any);
let error = every.set_env("WINEDLLOVERRIDES", "mscoree=b").err();
assert!(matches!(
    error,
    Some(FieldError::RetiredEnv {
        key: Key::DllOverrides,
        ..
    })
));
assert_eq!(retired_env("WineDebug"), Some(Key::DebugChannels));
assert!(inherited_conflicts().contains(&"WINEDLLOVERRIDES"));

Modules§

builtin
The built-in rules, compiled into compatdb.so.
runtime
What compatdb.so needs to apply a Resolution inside a process.

Structs§

Argument
Text appended to the command line: not empty and without a NUL.
ArgumentsDropped
An arguments_override that dropped the arguments the rules folded before it had accumulated.
DebugChannel
One WINEDEBUG entry of a rule, such as +relay or warn-heap: not empty, without , and without a NUL. The text is kept as written; Wine reads it.
Diagnostic
One problem with the value of ENV_VAR: the line, the field as it was written, if the problem is with one, and what is wrong.
DllOverride
One WINEDLLOVERRIDES element, <modules>=<order>.
DllOverrideReset
What a <module>=default entry of dll_overrides did.
EnvEntry
One environment entry of a rule: a variable and the value it gets, or no value, which removes the variable.
EnvFrom
Where an env entry of a Resolution came from.
EnvName
The name of an environment variable: not empty, without = and without a NUL. It is kept as written; a process environment matches it ASCII-case-insensitively.
EnvStep
What one env entry of a Resolution does in a process.
ExeName
The image basename of an Exe::Name: not empty, not * and without a NUL. Exe::new makes one.
Fingerprint
A fingerprint value: a substring that one string of the image’s version resource must contain, compared ASCII-case-insensitively. Any text without a NUL; an empty one matches every image.
ParseError
Why Table::parse_strict refused a value of ENV_VAR: every problem it found, in line order.
Resolution
The settings that apply to one process.
Rule
One rule of the table: what it matches, and the settings it applies to a process that matches. A rule has no name; exe is its only required matcher, and everything else is optional.
ScalarState
Where a scalar setting of a Resolution ended up.
Table
The whole database: the built-in rules followed by the rules of ENV_VAR, in the order they were declared, each with its Source.
VersionInfo
The parts of a PE version resource the database can match on. Vendor-set at link time, so stable across install location and patches.

Enums§

D3d9
Which D3D9 implementation a process loads. Independent of Dxgi: neither implementation’s d3d9.dll touches dxgi.dll.
DocSection
A section of the crate’s front page, the spec of the format and the rules.
Dxgi
Which implementation of the DXGI family a process loads.
Exe
The executable a rule matches: every process, or the processes whose image basename is a name, compared ASCII-case-insensitively.
FieldError
Why a field of a rule cannot be read, or a value cannot be set.
Key
The key of a field of a rule, which is also the name of the field on the wire.
KeyKind
What kind of field a Key names.
OpenGl
Which OpenGL the Mac driver gives a process.
Origin
Where a value of a Resolution came from: the rule that set it, by its Source and, for a built-in rule that does not match every process, its matchers.
Scalar
A setting that holds one value (see KeyKind::Scalar), or is reset with default (see Setting::Reset).
ScalarValue
The value of a scalar setting.
Setting
What a rule says about a setting that holds one value.
Severity
How a launcher treats a FieldError from its own settings, as FieldError::severity gives it.
Source
Where a rule of a Table comes from. It decides how the log names the rule’s values (see Origin) and whether its env entries are defaults (see Origin::env_is_default). Never serialized.
Specificity
How specific a rule’s matchers are, least specific first (see Table::resolve for how it orders the rules with their source).
ValueKind
The values a scalar setting takes.
Wined3d
Which renderer wined3d, Wine’s own Direct3D, draws with in a process.

Constants§

DEFAULT_VALUE
The value of a scalar setting that resets it (see Setting::Reset), and of a dll_overrides entry that removes the overrides of its modules.
DOCS_URL
Where this crate’s rustdoc is published. Key::docs_url and DocSection::url link into it.
ENV_VAR
The environment variable compatdb.so reads extra rules from. Wine itself never looks at it; it is only the channel from whatever started the process tree to the library.
METAL_HUD_ENV
The variables that show the Metal performance HUD, each with the value that metal_hud set to true gives it.

Functions§

check_env_name
Check name as the name of a rule’s environment entry, with the errors Rule::set_env gives for it.
inherited_conflicts
The variables a launcher removes from the environment it starts Wine with, when it inherited them, so that the launch does not depend on where the launcher itself was started from.
metal_hud_env
The environment entries metal_hud stands for.
retired_env
The setting that replaced the environment variable name, if one did.
retired_envs
The environment variables that a setting replaced, each with the key of that setting: the names for which retired_env gives that key and check_env_name refuses.
retired_keys
The setting names that are no longer read, each with how Key::from_setting treats it: the names it refuses with FieldError::RetiredKey and that severity.
retired_values
The values that are no longer read, each with its key and how Rule::set treats it: the values it refuses with FieldError::RetiredValue and that severity.