simple_env

Architecture

Architecture Overview

simple_env provides a clean Eiffel interface to environment variables using direct Win32 API calls. Unlike the standard Eiffel EXECUTION_ENVIRONMENT, it's designed specifically for SCOOP compatibility and uses inline C for performance.

+------------------+
|   SIMPLE_ENV     |
|   (Facade)       |
+------------------+
| + get()          |
| + set()          |
| + unset()        |
| + expand()       |
| + has()          |
| + all_names()    |
+------------------+
        |
        | Inline C externals
        v
+------------------+
|  simple_env.h    |
|  (C Header)      |
+------------------+
| se_get_env()     |
| se_set_env()     |
| se_unset_env()   |
| se_expand_env()  |
| se_env_exists()  |
+------------------+
        |
        v
+------------------+
|   Win32 API      |
+------------------+
| GetEnvironment   |
|   VariableA()    |
| SetEnvironment   |
|   VariableA()    |
| ExpandEnvironment|
|   StringsA()     |
+------------------+

Design Decisions

Why Not Use EXECUTION_ENVIRONMENT?

The standard Eiffel EXECUTION_ENVIRONMENT class has several limitations:

Why Direct Win32 API?

Why Inline C Pattern?

Following Eric Bezault's inline C pattern, all C code is embedded directly in the Eiffel source:

C Implementation

Header File Structure

The simple_env.h header contains all C implementations:

// simple_env.h - Environment variable operations
#include <windows.h>
#include <stdlib.h>
#include <string.h>

// Get environment variable value
// Returns allocated string (caller must free) or NULL
static inline char* se_get_env(const char* name) {
    DWORD size = GetEnvironmentVariableA(name, NULL, 0);
    if (size == 0) return NULL;

    char* buffer = (char*)malloc(size);
    if (buffer == NULL) return NULL;

    GetEnvironmentVariableA(name, buffer, size);
    return buffer;
}

// Set environment variable
// Returns 1 on success, 0 on failure
static inline int se_set_env(const char* name, const char* value) {
    return SetEnvironmentVariableA(name, value) ? 1 : 0;
}

// Unset environment variable
static inline int se_unset_env(const char* name) {
    return SetEnvironmentVariableA(name, NULL) ? 1 : 0;
}

// Expand environment strings (%VAR% -> value)
static inline char* se_expand_env(const char* input) {
    DWORD size = ExpandEnvironmentStringsA(input, NULL, 0);
    if (size == 0) return NULL;

    char* buffer = (char*)malloc(size);
    if (buffer == NULL) return NULL;

    ExpandEnvironmentStringsA(input, buffer, size);
    return buffer;
}

// Check if variable exists
static inline int se_env_exists(const char* name) {
    DWORD size = GetEnvironmentVariableA(name, NULL, 0);
    return (size > 0 || GetLastError() != ERROR_ENVVAR_NOT_FOUND) ? 1 : 0;
}

Memory Management

The C functions allocate memory that must be freed by the caller:

-- Eiffel side: always free allocated memory
l_result := c_se_get_env (l_name.item)
if l_result /= default_pointer then
    Result := pointer_to_string (l_result)
    c_free (l_result)  -- Essential: prevent memory leak
end

Eiffel External Bindings

Inline C Pattern

feature {NONE} -- C externals

    c_se_get_env (a_name: POINTER): POINTER
            -- Get environment variable value. Caller must free result.
        external "C inline use %"simple_env.h%""
        alias "return se_get_env((const char*)$a_name);"
        end

    c_se_set_env (a_name, a_value: POINTER): INTEGER
            -- Set environment variable. Returns 1 on success.
        external "C inline use %"simple_env.h%""
        alias "return se_set_env((const char*)$a_name, (const char*)$a_value);"
        end

    c_se_expand_env (a_input: POINTER): POINTER
            -- Expand environment strings. Caller must free result.
        external "C inline use %"simple_env.h%""
        alias "return se_expand_env((const char*)$a_input);"
        end

    c_free (a_ptr: POINTER)
            -- Free allocated memory.
        external "C inline use <stdlib.h>"
        alias "free($a_ptr);"
        end

String Conversion

Eiffel strings are converted to C strings using C_STRING:

get (a_name: READABLE_STRING_GENERAL): detachable STRING_32
    local
        l_name: C_STRING
        l_result: POINTER
    do
        -- Convert Eiffel string to C string
        create l_name.make (a_name.to_string_8)

        -- Call C function
        l_result := c_se_get_env (l_name.item)

        -- Convert result back to Eiffel string
        if l_result /= default_pointer then
            Result := pointer_to_string (l_result)
            c_free (l_result)
        end
    end

Variable Enumeration

Implementation Strategy

Enumerating all environment variables requires accessing the process environment block:

// C implementation: Get all variable names
char* se_get_all_names(void) {
    char* env_block = GetEnvironmentStringsA();
    if (env_block == NULL) return NULL;

    // Count total size needed
    size_t total_size = 0;
    char* ptr = env_block;
    while (*ptr) {
        char* eq = strchr(ptr, '=');
        if (eq && eq != ptr) {
            total_size += (eq - ptr) + 1;  // name + null
        }
        ptr += strlen(ptr) + 1;
    }
    total_size++;  // Final null terminator

    // Allocate and copy names only
    char* result = (char*)malloc(total_size);
    // ... copy logic ...

    FreeEnvironmentStringsA(env_block);
    return result;
}

Eiffel Parsing

The Eiffel side parses the null-separated string block:

all_names: ARRAYED_LIST [STRING_32]
    local
        l_ptr: POINTER
        l_pos: INTEGER
        l_name: STRING_8
        l_char: CHARACTER
    do
        create Result.make (100)
        l_ptr := c_se_get_all_names

        if l_ptr /= default_pointer then
            from
                l_pos := 0
                create l_name.make_empty
            until
                -- Double null = end of block
                c_char_at (l_ptr, l_pos) = '%U' and
                c_char_at (l_ptr, l_pos + 1) = '%U'
            loop
                l_char := c_char_at (l_ptr, l_pos)
                if l_char = '%U' then
                    -- End of one name
                    if not l_name.is_empty then
                        Result.extend (l_name.to_string_32)
                        create l_name.make_empty
                    end
                else
                    l_name.append_character (l_char)
                end
                l_pos := l_pos + 1
            end
            c_free (l_ptr)
        end
    end

SCOOP Compatibility

Design Principles

simple_env is designed for SCOOP from the ground up:

Safe Usage Pattern

local
    env: separate SIMPLE_ENV
do
    create env
    use_environment (env)
end

use_environment (a_env: separate SIMPLE_ENV)
    local
        config_path: STRING_32
    do
        -- String must be copied for use outside separate block
        if attached a_env.get ("CONFIG") as v then
            config_path := v.twin
        end

        -- Now config_path can be used outside the handler
    end

Why EXECUTION_ENVIRONMENT Isn't SCOOP-Safe

-- EXECUTION_ENVIRONMENT uses internal caching:
-- 1. First call: reads from system, stores in cache
-- 2. Second call: returns cached value
-- Problem: cache is shared mutable state!

-- SIMPLE_ENV: no caching, always direct API call
-- 1. Every call: reads directly from system
-- 2. No shared state between calls

Design by Contract

Preconditions

get (a_name: READABLE_STRING_GENERAL): detachable STRING_32
    require
        name_not_empty: not a_name.is_empty

set (a_name, a_value: READABLE_STRING_GENERAL)
    require
        name_not_empty: not a_name.is_empty

expand (a_string: READABLE_STRING_GENERAL): STRING_32
    require
        string_not_empty: not a_string.is_empty

Postconditions

set (a_name, a_value: READABLE_STRING_GENERAL)
    ensure
        variable_set: last_operation_succeeded implies attached get (a_name)

unset (a_name: READABLE_STRING_GENERAL)
    ensure
        variable_removed: last_operation_succeeded implies not has (a_name)

all_names: ARRAYED_LIST [STRING_32]
    ensure
        result_attached: Result /= Void

Win32 API Reference

Win32 Function SIMPLE_ENV Feature Purpose
GetEnvironmentVariableA get, has Retrieve variable value
SetEnvironmentVariableA set, unset Set or remove variable
ExpandEnvironmentStringsA expand Expand %VAR% references
GetEnvironmentStringsA all_names Get environment block
FreeEnvironmentStringsA all_names (internal) Free environment block

Note: The "A" suffix indicates ANSI (8-bit) character versions. Unicode (UTF-16) versions exist with "W" suffix but are not currently used.