Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Environment.GetEnvironmentVariables for Apple platforms using official API on iOS/tvOS/MacCatalyst #58161

Merged
merged 11 commits into from
Aug 27, 2021
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Runtime.InteropServices;

internal static partial class Interop
{
internal unsafe partial class Sys
{
[DllImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetEnv")]
internal static extern unsafe IntPtr GetEnv(string name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Runtime.InteropServices;

internal static partial class Interop
{
internal unsafe partial class Sys
{
[DllImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_GetEnviron")]
internal static extern unsafe IntPtr GetEnviron();

[DllImport(Interop.Libraries.SystemNative, EntryPoint = "SystemNative_FreeEnviron")]
internal static extern unsafe void FreeEnviron(IntPtr environ);
}
}
6 changes: 6 additions & 0 deletions src/libraries/Native/Unix/System.Native/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ else()
list (APPEND NATIVE_SOURCES pal_autoreleasepool.c)
endif()

if (CLR_CMAKE_TARGET_MACCATALYST OR CLR_CMAKE_TARGET_IOS OR CLR_CMAKE_TARGET_TVOS)
list (APPEND NATIVE_SOURCES pal_environment.m)
else()
list (APPEND NATIVE_SOURCES pal_environment.c)
endif()

if (CLR_CMAKE_TARGET_MACCATALYST OR CLR_CMAKE_TARGET_IOS OR CLR_CMAKE_TARGET_TVOS)
set(NATIVE_SOURCES ${NATIVE_SOURCES}
pal_log.m
Expand Down
4 changes: 4 additions & 0 deletions src/libraries/Native/Unix/System.Native/entrypoints.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "pal_autoreleasepool.h"
#include "pal_console.h"
#include "pal_datetime.h"
#include "pal_environment.h"
#include "pal_errno.h"
#include "pal_interfaceaddresses.h"
#include "pal_io.h"
Expand Down Expand Up @@ -258,6 +259,9 @@ static const Entry s_sysNative[] =
DllImportEntry(SystemNative_SetPosixSignalHandler)
DllImportEntry(SystemNative_GetPlatformSignalNumber)
DllImportEntry(SystemNative_GetGroups)
DllImportEntry(SystemNative_GetEnv)
DllImportEntry(SystemNative_GetEnviron)
DllImportEntry(SystemNative_FreeEnviron)
};

EXTERN_C const void* SystemResolveDllImport(const char* name);
Expand Down
32 changes: 32 additions & 0 deletions src/libraries/Native/Unix/System.Native/pal_environment.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#include "pal_config.h"
#include "pal_environment.h"

#include <stdlib.h>
#include <string.h>
#if HAVE_NSGETENVIRON
#include <crt_externs.h>
#endif

char* SystemNative_GetEnv(const char* variable)
{
return getenv(variable);
}

char** SystemNative_GetEnviron()
{
#if HAVE_NSGETENVIRON
return *(_NSGetEnviron());
#else
extern char **environ;
return environ;
#endif
}

void SystemNative_FreeEnviron(char** environ)
{
// no op
(void)environ;
}
13 changes: 13 additions & 0 deletions src/libraries/Native/Unix/System.Native/pal_environment.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma once

#include "pal_compiler.h"
#include "pal_types.h"

PALEXPORT char* SystemNative_GetEnv(const char* variable);

PALEXPORT char** SystemNative_GetEnviron(void);

PALEXPORT void SystemNative_FreeEnviron(char** environ);
78 changes: 78 additions & 0 deletions src/libraries/Native/Unix/System.Native/pal_environment.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#include "pal_config.h"
#include "pal_environment.h"

#include <Foundation/Foundation.h>
#include <CoreFoundation/CoreFoundation.h>
#include <objc/runtime.h>
#include <objc/message.h>

char* SystemNative_GetEnv(const char* variable)
{
return getenv(variable);
}

static char *empty_key_value_pair = "=";

static void get_environ_helper(const void *key, const void *value, void *context)
{
char ***temp_environ_ptr = (char***)context;
const char *utf8_key = [(NSString *)key UTF8String];
const char *utf8_value = [(NSString *)value UTF8String];
int utf8_key_length = strlen(utf8_key);
int utf8_value_length = strlen(utf8_value);
char *key_value_pair;

key_value_pair = malloc(utf8_key_length + utf8_value_length + 2);
if (key_value_pair != NULL)
{
strcpy(key_value_pair, utf8_key);
key_value_pair[utf8_key_length] = '=';
strcpy(key_value_pair + utf8_key_length + 1, utf8_value);
}
else
{
// In case of failed allocation add pointer to preallocated entry. This is
// ignored on the managed side and skipped over in SystemNative_FreeEnviron.
key_value_pair = empty_key_value_pair;
}

**temp_environ_ptr = key_value_pair;
(*temp_environ_ptr)++;
}

char** SystemNative_GetEnviron()
{
char **temp_environ;
char **temp_environ_ptr;

CFDictionaryRef environment = (CFDictionaryRef)[[NSProcessInfo processInfo] environment];
int count = CFDictionaryGetCount(environment);
temp_environ = (char **)malloc((count + 1) * sizeof(char *));
if (temp_environ != NULL)
{
temp_environ_ptr = temp_environ;
CFDictionaryApplyFunction(environment, get_environ_helper, &temp_environ_ptr);
*temp_environ_ptr = NULL;
}

return temp_environ;
}

void SystemNative_FreeEnviron(char** environ)
{
if (environ != NULL)
{
for (char** environ_ptr = environ; *environ_ptr != NULL; environ_ptr++)
{
if (*environ_ptr != empty_key_value_pair)
{
free(*environ_ptr);
}
}

free(environ);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,12 @@
<ItemGroup Condition="'$(TargetsUnix)' == 'true' or '$(TargetsBrowser)' == 'true'">
<Compile Include="$(BclSourcesRoot)\System\Environment.Unix.Mono.cs" />
<Compile Include="$(BclSourcesRoot)\System\Threading\LowLevelLifoSemaphore.Unix.Mono.cs" />
<Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.GetEnv.cs">
<Link>Common\Interop\Unix\System.Native\Interop.GetEnv.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.GetEnviron.cs">
<Link>Common\Interop\Unix\System.Native\Interop.GetEnviron.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup Condition="'$(TargetsMacCatalyst)' == 'true' or '$(TargetsiOS)' == 'true' or '$(TargetstvOS)' == 'true'">
<Compile Include="$(BclSourcesRoot)\System\Environment.iOS.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Mono;

namespace System
{
Expand All @@ -20,7 +20,7 @@ public partial class Environment

if (s_environment == null)
{
return InternalGetEnvironmentVariable(variable);
return Marshal.PtrToStringAnsi(Interop.Sys.GetEnv(variable));
}

variable = TrimStringOnFirstZero(variable);
Expand All @@ -31,23 +31,15 @@ public partial class Environment
}
}

private static string InternalGetEnvironmentVariable(string name)
{
using (SafeStringMarshal handle = RuntimeMarshal.MarshalString(name))
{
return internalGetEnvironmentVariable_native(handle.Value);
}
}

private static unsafe void SetEnvironmentVariableCore(string variable, string? value)
{
Debug.Assert(variable != null);

EnsureEnvironmentCached();
variable = TrimStringOnFirstZero(variable);
value = value == null ? null : TrimStringOnFirstZero(value);
lock (s_environment!)
{
variable = TrimStringOnFirstZero(variable);
value = value == null ? null : TrimStringOnFirstZero(value);
if (string.IsNullOrEmpty(value))
{
s_environment.Remove(variable);
Expand Down Expand Up @@ -97,22 +89,75 @@ private static Dictionary<string, string> GetSystemEnvironmentVariables()
{
var results = new Dictionary<string, string>();

foreach (string name in GetEnvironmentVariableNames())
IntPtr block = Interop.Sys.GetEnviron();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is straight copy from NativeAOT.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then I think it would be better to have it in shared location

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally I would agree.

I started with a minimal fix because I wanted to limit the impact for backporting to .NET 6 branch.

Then I moved it to System.Native based on chat with @akoeplinger (and I agree that it belongs there). Proper sharing of the code with NativeAOT will need few more changes and I wanted to validate the approach first before proceeding with that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think this is fine for now, we can unify with NativeAOT in main later on.

if (block != IntPtr.Zero)
{
if (name != null)
try
{
IntPtr blockIterator = block;

// Per man page, environment variables come back as an array of pointers to strings
// Parse each pointer of strings individually
while (ParseEntry(blockIterator, out string? key, out string? value))
{
if (key != null && value != null)
{
try
{
// Add may throw if the environment block was corrupted leading to duplicate entries.
// We allow such throws and eat them (rather than proactively checking for duplication)
// to provide a non-fatal notification about the corruption.
results.Add(key, value);
}
catch (ArgumentException) { }
}

// Increment to next environment variable entry
blockIterator += IntPtr.Size;
}
}
finally
{
results.Add(name, InternalGetEnvironmentVariable(name));
Interop.Sys.FreeEnviron(block);
}
}

return results;
}


[MethodImplAttribute(MethodImplOptions.InternalCall)]
private static extern string internalGetEnvironmentVariable_native(IntPtr variable);

[MethodImplAttribute(MethodImplOptions.InternalCall)]
private static extern string[] GetEnvironmentVariableNames();
// Use a local, unsafe function since we cannot use `yield return` inside of an `unsafe` block
static unsafe bool ParseEntry(IntPtr current, out string? key, out string? value)
{
// Setup
key = null;
value = null;

// Point to current entry
byte* entry = *(byte**)current;

// Per man page, "The last pointer in this array has the value NULL"
// Therefore, if entry is null then we're at the end and can bail
if (entry == null)
return false;

// Parse each byte of the entry until we hit either the separator '=' or '\0'.
// This finds the split point for creating key/value strings below.
// On some old OS, the environment block can be corrupted.
danmoseley marked this conversation as resolved.
Show resolved Hide resolved
// Some will not have '=', so we need to check for '\0'.
byte* splitpoint = entry;
while (*splitpoint != '=' && *splitpoint != '\0')
splitpoint++;

// Skip over entries starting with '=' and entries with no value (just a null-terminating char '\0')
if (splitpoint == entry || *splitpoint == '\0')
return true;

// The key is the bytes from start (0) until our splitpoint
key = new string((sbyte*)entry, 0, checked((int)(splitpoint - entry)));
// The value is the rest of the bytes starting after the splitpoint
value = new string((sbyte*)(splitpoint + 1));

return true;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private static string GetFolderPathCore(SpecialFolder folder, SpecialFolderOptio
return Interop.Sys.SearchPath(NSSearchPathDirectory.NSCachesDirectory);

case SpecialFolder.UserProfile:
return InternalGetEnvironmentVariable("HOME");
return GetEnvironmentVariable("HOME");

case SpecialFolder.CommonApplicationData:
return "/usr/share";
Expand Down
2 changes: 0 additions & 2 deletions src/mono/mono/metadata/icall-def-netcore.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,10 @@ ICALL_TYPE(ENV, "System.Environment", ENV_1)
NOHANDLES(ICALL(ENV_1, "Exit", ves_icall_System_Environment_Exit))
HANDLES(ENV_1a, "FailFast", ves_icall_System_Environment_FailFast, void, 3, (MonoString, MonoException, MonoString))
HANDLES(ENV_2, "GetCommandLineArgs", ves_icall_System_Environment_GetCommandLineArgs, MonoArray, 0, ())
HANDLES(ENV_3, "GetEnvironmentVariableNames", ves_icall_System_Environment_GetEnvironmentVariableNames, MonoArray, 0, ())
NOHANDLES(ICALL(ENV_4, "GetProcessorCount", ves_icall_System_Environment_get_ProcessorCount))
NOHANDLES(ICALL(ENV_9, "get_ExitCode", mono_environment_exitcode_get))
NOHANDLES(ICALL(ENV_15, "get_TickCount", ves_icall_System_Environment_get_TickCount))
NOHANDLES(ICALL(ENV_15a, "get_TickCount64", ves_icall_System_Environment_get_TickCount64))
HANDLES(ENV_17, "internalGetEnvironmentVariable_native", ves_icall_System_Environment_GetEnvironmentVariable_native, MonoString, 1, (const_char_ptr))
NOHANDLES(ICALL(ENV_20, "set_ExitCode", mono_environment_exitcode_set))

ICALL_TYPE(GC, "System.GC", GC_13)
Expand Down
Loading