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

Initial Windows support for Ark #140

Merged
merged 39 commits into from
Dec 7, 2023
Merged

Initial Windows support for Ark #140

merged 39 commits into from
Dec 7, 2023

Conversation

DavisVaughan
Copy link
Contributor

@DavisVaughan DavisVaughan commented Nov 7, 2023

Branched from #172

This PR does the initial work to get Windows support up and running enough that I think we can merge it in. That will allow us to then iterate on a few things at the same time - including getting a GitHub Actions workflow that builds an executable for us.

I'd like to take this chance to document some Windows peculiarities that we have to work around. It may be useful for later.

Platform specific folder structure

We now have a decent chunk of platform specific code, so I've introduced a new convention to handle this. Each crate has the following structure:

  • sys.rs
  • sys/unix.rs
  • sys/windows.rs
  • sys/unix/
  • sys/windows/

In sys.rs, we always do:

cfg_if::cfg_if! {
    if #[cfg(unix)] {
        mod unix;
        pub use self::unix::*;
    } else if #[cfg(windows)] {
        mod windows;
        pub use self::windows::*;
    }
}

This allows us to create 2 identical APIs in sys/unix.rs and sys/windows.rs, and then use them in a platform agnostic way with sys::module::fn() from outside of sys/. This is inspired by the Rust std library.

Write console buffer on Windows

On Windows, the write_console() buffer that R provides us has some weird quirks. In general, the buffer that comes through will be in the system encoding, so we typically need to convert it to UTF-8 to then put it in a rust string and forward it to the frontend. Okay, no problem.

The weirdness is that if EmbeddedUTF8 is set for R, then R surrounds UTF-8 text with a 3-byte escape like \x02\xFF\xFE<text>\x03\xFF\xFE to "mark" it. There could be more text in the buffer before or after this that is just in the system encoding. We have to break the buffer into the system encoded bits and the UTF-8 text bits, re-encode the system encoded bits to UTF-8, drop the 3-byte escapes, and put everything back together. This is done in console_to_utf8().

Interrupts

On the unix side, we use R_pending_interrupts to flag when R should handle an interrupt. On the windows side, this is apparently called UserBreak, and is mostly handled the same way, except I don't see R_suspended_interrupts for Windows.

Global variables from libR-sys

libR-sys exposes both functions and global variables from R for us to use. However, either it, bindgen, or rustc have an issue where global variables bindings are not being written in such a way that they work for Windows. Take R_NilValue for example. It is exposed in libR-sys through:

extern "C" {
  pub static mut R_NilValue: SEXP;
}

And libR-sys sets println!("cargo:rustc-link-lib=dylib=R");. Now, AFAICT that should be enough because it tells rustc to link to R dynamically.

And it does work fine on Mac, but on Windows global variables MUST be marked with dllimport otherwise the linker cannot find them, and this does not seem to be happening with this setup. You must also add:

#[link(name = "R", kind = "dylib")]
extern "C" {
  pub static mut R_NilValue: SEXP;
}

If you do that, then it works right on Windows. This causes quite an issue because we import many global variables from R.

To work around this, I have created a new subcrate, libR-shim, in this PR #172. It re-exports everything we need from libR-sys except for the global variables, which it then manually provides bindings for tagged with the additional #link[] attribute. This wasn't fun to do, but it seemed like the best way.

Practically, all this means is that we now import libR_shim::* instead of libR_sys::*, and if we are missing anything in the shim, we have to re-export it before we can use it.

libR-sys has been removed as a dependency from ark and harp, so you can't accidentally use libR_sys::R_NilValue, which would be bad.

R setup

On Unix, we have access to a number of ptr_* global variables that we use to set hooks like ReadConsole() (ptr_R_ReadConsole) and friends. On Windows, those don't actually exist. They would be declared in Rinterface.h, but they are behind a #ifdef R_INTERFACE_PTRS, and that is only defined on Unix.
https://github.com/wch/r-source/blob/55cd975c538ad5a086c2085ccb6a3037d5a0cb9a/src/include/Rinterface.h#L131

Instead, we set things like this through Rstart, a struct that holds startup options that is passed to R_DefParamsEx(), which sets up some initial options, and then R_SetParams(), which assigns the things in Rstart into global variables that R manages from then on.
https://github.com/wch/r-source/blob/55cd975c538ad5a086c2085ccb6a3037d5a0cb9a/src/include/R_ext/RStartup.h#L127-L129

The problem is, libR-sys doesn't include Rstart in the generated bindings on Windows. They would need to include #include <R_ext/RStartup.h> in their wrapper.h file here (https://github.com/extendr/libR-sys/blob/6fc2b5f2371fe20ec53d2cdf845a6e13dfdd3cfe/wrapper.h#L42), and they would also need to #define Win32 to generate the full Windows bindings for it.

For now, I have actually gone and done this manually using a fork of libR-sys. The generated bindings for Rstart lives in interface_types.rs, which we then use in interface.rs. Hopefully we can remove this in the future?

DLLs and LIBs

At cargo build time, ark / libR-sys need R.lib to exist at C:\\Program Files\\R\\R-4.3.2\\bin\\x64\\R.lib. But R doesn't ship with a lib file, it ships with R.dll. To workaround this, we use a script from RStudio called dll2lib.R (see the comment below) which uses Visual Studio utilities to generate the R.lib from the dll.

You typically have to set R_HOME before cargo build the first time you build the libR-sys dependency, I do that with

# command prompt
set R_HOME=C:\Program Files\R\R-4.3.2
# powershell
$env:R_HOME = "C:\Program Files\R\R-4.3.2"

After building it once, you don't need that to iterate on ark itself.

At ark load time, i.e. when the chosen version of R is being initialized, ark.exe needs to be able to dynamically link to the R.dll that goes with the user's R version. Windows uses the PATH as one place where it looks for libraries to dynamically link against, so in positron-r we add C:\\Program Files\\R\\R-<VERSION>\\bin\\x64 to the PATH of the subprocess that ark is run in.

Still TODO after merging

Outdated notes below, possibly still useful for historical purposes:

I got stuck for awhile at Rust's (rustc's) linking step where it threw out something like: "Can't find R.lib"

And indeed in the R_HOME/bin directory there is an R.dll but no R.lib. It seems like in RStudio for Windows they totally regenerate the R.lib on the fly using dll2lib.R?? It uses this script:
https://github.com/rstudio/rstudio/blob/d26c0d74ad873a0f48b9153c2b0e56111cae2932/src/cpp/tools/dll2lib.R

It's kind of bonkers, but this works (from Command Prompt):

  • Start Command Prompt with admin privileges
  • cd "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\"
  • VsDevCmd.bat -clean_env -no_logo
  • VsDevCmd.bat -arch=amd64 -startdir=none -host_arch=amd64 -winsdk=10.0.22000.0 -no_logo
  • Run dll2lib.R in an R session started from that Command Prompt session. Skip the Sys.setenv() line at the beginning.
  • You should end up with an R.lib (and some others) in your R_HOME/bin

(These commands were taken from https://github.com/rstudio/rstudio/blob/d26c0d74ad873a0f48b9153c2b0e56111cae2932/package/win32/make-package.bat#L179-L188, I think the idea is that it ends up putting some required locations on the path, i.e. for dumpbin.exe, lib.exe, and maybe cl.exe (a c compiler))

That allowed it to find R.lib, and now I am stuck with a new linker error:

  = note:    Creating library D:\Users\davis-vaughan\files\programming\positron\amalthea\target\debug\deps\ark.lib and object D:\Users\davis-vaughan\files\programming\positron\amalthea\target\debug\deps\ark.exp
          libark-80e335538b8b7a46.rlib(ark-80e335538b8b7a46.2tsak1yhuntf56wy.rcgu.o) : error LNK2019: unresolved external symbol R_running_as_main_program referenced in function _ZN3ark9interface7start_r17h6836a63f3dd11975E
          libark-80e335538b8b7a46.rlib(ark-80e335538b8b7a46.2tsak1yhuntf56wy.rcgu.o) : error LNK2019: unresolved external symbol ptr_R_WriteConsole referenced in function _ZN3ark9interface7start_r17h6836a63f3dd11975E
          libark-80e335538b8b7a46.rlib(ark-80e335538b8b7a46.2tsak1yhuntf56wy.rcgu.o) : error LNK2019: unresolved external symbol ptr_R_WriteConsoleEx referenced in function _ZN3ark9interface7start_r17h6836a63f3dd11975E
          libark-80e335538b8b7a46.rlib(ark-80e335538b8b7a46.2tsak1yhuntf56wy.rcgu.o) : error LNK2019: unresolved external symbol ptr_R_ShowMessage referenced in function _ZN3ark9interface7start_r17h6836a63f3dd11975E
          libark-80e335538b8b7a46.rlib(ark-80e335538b8b7a46.2tsak1yhuntf56wy.rcgu.o) : error LNK2019: unresolved external symbol ptr_R_Busy referenced in function _ZN3ark9interface7start_r17h6836a63f3dd11975E
          libark-80e335538b8b7a46.rlib(ark-80e335538b8b7a46.2tsak1yhuntf56wy.rcgu.o) : error LNK2019: unresolved external symbol R_wait_usec referenced in function _ZN3ark9interface7start_r17h6836a63f3dd11975E
          libark-80e335538b8b7a46.rlib(ark-80e335538b8b7a46.2tsak1yhuntf56wy.rcgu.o) : error LNK2019: unresolved external symbol R_PolledEvents referenced in function _ZN3ark9interface7start_r17h6836a63f3dd11975E
          libark-80e335538b8b7a46.rlib(ark-80e335538b8b7a46.3v147ityszidpou2.rcgu.o) : error LNK2001: unresolved external symbol R_PolledEvents
          libark-80e335538b8b7a46.rlib(ark-80e335538b8b7a46.2tsak1yhuntf56wy.rcgu.o) : error LNK2019: unresolved external symbol R_checkActivity referenced in function _ZN3ark9interface5RMain14process_events17h8139846d39dd97b5E
          libark-80e335538b8b7a46.rlib(ark-80e335538b8b7a46.2tsak1yhuntf56wy.rcgu.o) : error LNK2019: unresolved external symbol R_InputHandlers referenced in function _ZN3ark9interface5RMain14process_events17h8139846d39dd97b5E
          libark-80e335538b8b7a46.rlib(ark-80e335538b8b7a46.2tsak1yhuntf56wy.rcgu.o) : error LNK2019: unresolved external symbol R_runHandlers referenced in function _ZN3ark9interface5RMain14process_events17h8139846d39dd97b5E
          D:\Users\davis-vaughan\files\programming\positron\amalthea\target\debug\deps\ark.exe : fatal error LNK1120: 11 unresolved externals

I think we knew this was coming. Things like ptr_R_WriteConsole are declared in an R header file, but are only defined in Unix specific files. We define them ourselves at runtime, but this is too late for Rust's linker check, I think 😢

On Windows there is no -undefined dynamic_linking flag that we can set, so we need to do something else here, like work with the Rstart struct and set these pointers through that (although that is also tricky because it has changed over time, most recently in R 4.2.0, and RStudio does some crazy stuff there to support many versions)

@DavisVaughan
Copy link
Contributor Author

DavisVaughan commented Nov 21, 2023

Figured out where the dumpbin.exe and lib.exe executables live so I got dll2lib.R down to 1 file to run

path <- "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.37.32822\\bin\\Hostx86\\x86"

if (!dir.exists(path)) {
  stop("Visual Studio tools directory is incorrect or the tools have not been installed.")
}

# Put the path containing the tools on the PATH.
Sys.setenv(PATH = paste(path, Sys.getenv("PATH"), sep = ";"))

# Find R DLLs.
dlls <- list.files(R.home("bin"), pattern = "dll$", full.names = TRUE)

message("Generating .lib files for DLLs in ", R.home("bin"))

# Generate corresponding 'lib' file for each DLL.
for (dll in dlls) {

   # check to see if we've already generated our exports
   def <- sub("dll$", "def", dll)
   if (file.exists(def))
      next

   # Call it on R.dll to generate exports.
   command <- sprintf("dumpbin.exe /EXPORTS /NOLOGO %s", dll)
   message("> ", command)
   output <- system(paste(command), intern = TRUE)

   # Remove synonyms.
   output <- sub("=.*$", "", output)

   # Find start, end markers
   start <- grep("ordinal\\s+hint\\s+RVA\\s+name", output)
   end <- grep("^\\s*Summary\\s*$", output)
   contents <- output[start:(end - 1)]
   contents <- contents[nzchar(contents)]

   # Remove forwarded fields (not certain that this does anything)
   contents <- grep("forwarded to", contents, invert = TRUE, value = TRUE, fixed = TRUE)

   # parse into a table
   tbl <- read.table(text = contents, header = TRUE, stringsAsFactors = FALSE)
   exports <- tbl$name

   # sort and re-format exports
   exports <- sort(exports)
   exports <- c("EXPORTS", paste("\t", tbl$name, sep = ""))

   # Write the exports to a def file
   def <- sub("dll$", "def", dll)
   cat(exports, file = def, sep = "\n")

   # Call 'lib.exe' to generate the library file.
   outfile <- sub("dll$", "lib", dll)
   fmt <- "lib.exe /def:%s /out:%s /machine:%s"
   cmd <- sprintf(fmt, def, outfile, .Platform$r_arch)
   system(cmd)

}

Comment on lines +8 to +16
cfg_if::cfg_if! {
if #[cfg(unix)] {
mod unix;
pub use self::unix::*;
} else if #[cfg(windows)] {
mod windows;
pub use self::windows::*;
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Each crate now has a sys.rs file that looks like this. Typically the way this works is that, for example, the crate level signals module will call sys::signals::fn() for a platform specific version of fn() that it needs. i.e. handle_interrupt_request() is one such fn().

You typically won't call sys::signals::* outside of the signals module. The signals module itself then either re-exposes functions through pub use or exposes its own slightly higher level wrappers around the system specific utilities.

So then other files call crate::signals::*

This is modeled after the Rust std library

}

pub fn listen(&self) {
// TODO: Windows
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Stub for stream capturing on Windows. A big TODO, but not critical to "just make it work"

Comment on lines -504 to -526
fn initialize_signal_handlers() {
// Reset the signal block.
//
// This appears to be necessary on macOS; 'sigprocmask()' specifically
// blocks the signals in _all_ threads associated with the process, even
// when called from a spawned child thread. See:
//
// https://github.com/opensource-apple/xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/bsd/kern/kern_sig.c#L1238-L1285
// https://github.com/opensource-apple/xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/bsd/kern/kern_sig.c#L796-L839
//
// and note that 'sigprocmask()' uses 'block_procsigmask()' to apply the
// requested block to all threads in the process:
//
// https://github.com/opensource-apple/xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/bsd/kern/kern_sig.c#L571-L599
//
// We may need to re-visit this on Linux later on, since 'sigprocmask()' and
// 'pthread_sigmask()' may only target the executing thread there.
//
// The behavior of 'sigprocmask()' is unspecified after all, so we're really
// just relying on what the implementation happens to do.
let mut sigset = SigSet::empty();
sigset.add(SIGINT);
sigprocmask(SigmaskHow::SIG_BLOCK, Some(&sigset), None).unwrap();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When you see a big deletion, it didn't get removed, just moved to the unix/* equivalent file, i.e. unix/interface.rs

crates/ark/src/interface.rs Outdated Show resolved Hide resolved
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Unix specific startup code from interface.rs has been moved here. Not much has changed really. Unix specific callables from the R api are imported at the bottom of here now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Manually generated bindings for Rstart using a forked version of libR-sys

I'm not entirely sure how this is going to behave on different versions of R. Hopefully it "just works" since it has C representation and older versions of R simply won't access fields like EmitEmbeddedUTF8, added in R 4.0.0

(Similar to what RStudio does)

Comment on lines 11 to 17
pub fn initialize_signal_handlers() {
// TODO: Windows
}

pub fn initialize_signal_block() {
// TODO: Windows
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Uncertain if we actually need these

Copy link
Contributor

Choose a reason for hiding this comment

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

Signal masking/blocking is POSIX only.

We don't need a SIGINT handler since we can set the global pending var from the Control thread right? Technically we don't need this handler on Unix either, but I guess we want to support sending interrupts from e.g. a terminal. Though we could just let the default SIGINT handler from R set up for that purpose?

Comment on lines +19 to +34
pub fn interrupts_pending() -> bool {
unsafe { UserBreak == Rboolean_TRUE }
}

pub fn set_interrupts_pending(pending: bool) {
if pending {
unsafe { UserBreak = Rboolean_TRUE };
} else {
unsafe { UserBreak = Rboolean_FALSE };
}
}

#[link(name = "R", kind = "dylib")]
extern "C" {
static mut UserBreak: libR_shim::Rboolean;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Apparently UserBreak is the Windows equivalent (ish) to R_pending_interrupts, so I've created some helpers to wrap these in a platform agnostic way, and we use those everywhere now

Comment on lines 23 to 31
let x = MultiByteToWideChar(code_page, flags, x)?;

// `WC::NoValue` doesn't exist, so we make it unsafely:
// https://github.com/rodrigocfd/winsafe/issues/110
let flags = unsafe { WC::from_raw(0) };
let default_char = None;
let used_default_char = None;

let x = WideCharToMultiByte(CP::UTF8, flags, &x, default_char, used_default_char)?;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I currently have a locally patched version of winsafe for rodrigocfd/winsafe#111

It won't actually break anything if you don't have a patched version, console output just won't look quite right (extra spaces and new lines typically)

*
*/

pub use sys::line_ending::NATIVE_LINE_ENDING;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I took the opportunity to move this platform specific code into the new folder structure too

@DavisVaughan DavisVaughan marked this pull request as ready for review December 5, 2023 20:11
@jennybc
Copy link
Member

jennybc commented Dec 6, 2023

With this PR and its positron companion (posit-dev/positron#1900) and following these instructions, I have successfully built ark and used R inside positron!

The only coffee table I banged my shins on is that, when you run the The Code Known As dll2lib.R (#140 (comment)), you need to do so as administrator. I saved that code to positron\extensions\positron-r\scripts for lack of any better ideas. I had to tweak the path a bit because I seem to have ended up with a slightly different (newer) Visual Studio build:

# What Davis used (subtle difference in ?version?)
# path <- "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.37.32822\\bin\\Hostx86\\x86"
path <- "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.38.33130\\bin\\Hostx86\\x86"

Then it's important to launch RStudio (or what have you) as administrator before you try to execute the code that creates the .lib files for the DLLs.

I also forgot to set the R_HOME env var before my first attempt to do cargo build, but that was pure user error. @DavisVaughan makes it clear that this is necessary.

@jennybc
Copy link
Member

jennybc commented Dec 6, 2023

The recent work on R_HOME and R_USER are working for me 🎉
And now I'm also getting the full (vs 8.3) path to R:

> R.home()
[1] "C:/Program Files/R/R-4.3.2"

@DavisVaughan DavisVaughan changed the title Windows testing Initial Windows support for Ark Dec 6, 2023
Copy link
Member

@jennybc jennybc left a comment

Choose a reason for hiding this comment

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

I am not reviewing the implementation, but rather confirming that I can build ARK on Windows from this branch and, together with posit-dev/positron#1900, can launch a dev build of Positron on Windows, with a functional R interpreter.

Copy link
Contributor

@lionel- lionel- left a comment

Choose a reason for hiding this comment

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

LGTM!

// TODO: Windows.
// TODO: Needs to send a SIGINT to the whole process group so that
// processes started by R will also be interrupted.
sys::control::handle_interrupt_request();
Copy link
Contributor

Choose a reason for hiding this comment

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

This namespace makes me think that it's from the stdlib rather than something from ark, but I guess I just need to get used to it.

crates/ark/src/interface.rs Outdated Show resolved Hide resolved
let mut sigset = SigSet::empty();
sigset.add(SIGINT);
sigprocmask(SigmaskHow::SIG_BLOCK, Some(&sigset), None).unwrap();
signals::initialize_signal_block();
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use ark::signals:: for this sort of stdlib-sounding namespaces? Same with traps:: and sys::. We're lucky because ark:: is very short so that might be a good convention for us.

@@ -0,0 +1,133 @@
/*
Copy link
Contributor

Choose a reason for hiding this comment

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

These abstractions look good

///
/// \002\377\376 <text> \003\377\376
///
/// strangely, we see these escapes around text that is not UTF-8
Copy link
Contributor

Choose a reason for hiding this comment

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

🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this was an rstudio comment i copied over

crates/ark/src/sys/windows/console.rs Outdated Show resolved Hide resolved
Comment on lines 11 to 17
pub fn initialize_signal_handlers() {
// TODO: Windows
}

pub fn initialize_signal_block() {
// TODO: Windows
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Signal masking/blocking is POSIX only.

We don't need a SIGINT handler since we can set the global pending var from the Control thread right? Technically we don't need this handler on Unix either, but I guess we want to support sending interrupts from e.g. a terminal. Though we could just let the default SIGINT handler from R set up for that purpose?

@jennybc
Copy link
Member

jennybc commented Dec 7, 2023

One more important thing I just ran into and then @DavisVaughan reminded me, from a slack conversation:

https://github.com/posit-dev/amalthea/blob/728dd25af69990369dfcd71e49781ddc091b61ed/crates/ark/src/r_task.rs#L92

This timeout is currently too short for us in the AWS Windows cloud machines (generally very sluggish and laggy for me) and the way you'll find out is you'll get crashes while trying to do normal things, like install a package. Increase from 5 to 5000 to get unstuck for now.

@DavisVaughan DavisVaughan changed the base branch from feature/libR-shim to main December 7, 2023 20:56
@DavisVaughan DavisVaughan merged commit 36e6fe6 into main Dec 7, 2023
1 check passed
@DavisVaughan DavisVaughan deleted the test/windows branch December 7, 2023 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants