diff --git a/Cargo.lock b/Cargo.lock
index f243ff42..57716f4b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -560,6 +560,7 @@ dependencies = [
  "serde_json",
  "tracing",
  "tracing-subscriber",
+ "winapi",
 ]
 
 [[package]]
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index c0c33acf..c1e6cf33 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -235,7 +235,7 @@ fn should_do_native_call_on_system_array_copy() {
     );
 }
 
-use crate::utils::get_output;
+use crate::utils::{assert_file, get_output};
 use std::time::{SystemTime, UNIX_EPOCH};
 
 #[test]
@@ -462,62 +462,39 @@ fn should_use_grandparent_method_via_super() {
     );
 }
 
-use std::fs;
-
 #[test]
 fn should_write_file_to_fs() {
-    let file_path = "tests/tmp/test.txt";
-
-    assert_success(
+    assert_file(
         "samples.io.fileoutputstreamexample.FileOutputStreamExample",
-        "",
-    );
-
-    assert!(fs::metadata(file_path).is_ok(), "File does not exist");
-    let content = fs::read_to_string(file_path).expect("Failed to read file");
-    assert_eq!(content, "CAFEBABE", "File content does not match");
-    fs::remove_file(file_path).expect("Failed to delete file");
+        "tests/tmp/test.txt",
+        "CAFEBABE",
+    )
 }
 
 #[test]
 fn should_write_file_to_fs_with_buffered_stream() {
-    let file_path = "tests/tmp/buffered_output.txt";
-
-    assert_success(
+    assert_file(
         "samples.io.bufferedoutputstreamchunkingexample.BufferedOutputStreamChunkingExample",
-        "",
-    );
-
-    assert!(fs::metadata(file_path).is_ok(), "File does not exist");
-    let content = fs::read_to_string(file_path).expect("Failed to read file");
-    assert_eq!(
-        content, "This is a test for BufferedOutputStream chunking.",
-        "File content does not match"
-    );
-    fs::remove_file(file_path).expect("Failed to delete file");
+        "tests/tmp/buffered_output.txt",
+        "This is a test for BufferedOutputStream chunking.",
+    )
 }
 
 #[test]
 fn should_write_file_with_print_stream() {
-    let file_path = "tests/tmp/print_stream_test.txt";
-
-    assert_success("samples.io.printstreamexample.PrintStreamExample", "");
-
-    assert!(fs::metadata(file_path).is_ok(), "File does not exist");
-    let content = fs::read_to_string(file_path).expect("Failed to read file");
-    assert_eq!(
-        content,
-        r#"Hello, PrintStream!
+    let expected_file_content = r#"Hello, PrintStream!
 First Line
 Second Line
 Third Line
 Hello as raw bytes
 This is written immediately. This follows after flush.
 This is an example of chaining PrintStreams.
-"#,
-        "File content does not match"
+"#;
+    assert_file(
+        "samples.io.printstreamexample.PrintStreamExample",
+        "tests/tmp/print_stream_test.txt",
+        expected_file_content,
     );
-    fs::remove_file(file_path).expect("Failed to delete file");
 }
 
 #[test]
diff --git a/tests/utils.rs b/tests/utils.rs
index daa33230..080a352c 100644
--- a/tests/utils.rs
+++ b/tests/utils.rs
@@ -1,20 +1,13 @@
 use assert_cmd::Command;
-use std::env;
+use std::{env, fs};
 
 const PATH: &str = "tests/test_data";
 
-fn get_command(entry: &str) -> Command {
-    let repo_path = env::current_dir().expect("Failed to get current directory");
-
-    let mut cmd = Command::cargo_bin("rusty-jvm").expect("Failed to locate rusty-jvm binary");
-    cmd.env("RUSTY_JAVA_HOME", repo_path)
-        .current_dir(PATH)
-        .arg(entry);
-    cmd
-}
-
 #[allow(dead_code)]
 pub fn assert_success(entry: &str, expected: &str) {
+    #[cfg(target_os = "windows")]
+    let expected = to_windows(expected);
+
     get_command(entry)
         .assert()
         .success()
@@ -29,3 +22,33 @@ pub fn get_output(entry: &str) -> String {
 
     String::from_utf8(output.stdout).expect("Failed to convert output to string")
 }
+
+pub fn assert_file(entry: &str, file_path: &str, expected_file_content: &str) {
+    assert_success(entry, "");
+
+    #[cfg(target_os = "windows")]
+    let expected_file_content = to_windows(expected_file_content);
+
+    assert!(fs::metadata(file_path).is_ok(), "File does not exist");
+    let content = fs::read_to_string(file_path).expect("Failed to read file");
+    assert_eq!(
+        content, expected_file_content,
+        "File content does not match"
+    );
+    fs::remove_file(file_path).expect("Failed to delete file");
+}
+
+fn get_command(entry: &str) -> Command {
+    let repo_path = env::current_dir().expect("Failed to get current directory");
+
+    let mut cmd = Command::cargo_bin("rusty-jvm").expect("Failed to locate rusty-jvm binary");
+    cmd.env("RUSTY_JAVA_HOME", repo_path)
+        .current_dir(PATH)
+        .arg(entry);
+    cmd
+}
+
+#[cfg(target_os = "windows")]
+fn to_windows(input: &str) -> String {
+    input.replace("\n", "\r\n")
+}
diff --git a/vm/Cargo.toml b/vm/Cargo.toml
index 56ff547b..1cb1a9f0 100644
--- a/vm/Cargo.toml
+++ b/vm/Cargo.toml
@@ -14,3 +14,4 @@ tracing = "0.1.40"
 tracing-subscriber = { version = "0.3.18", features = ["env-filter"]}
 num-traits = "0.2.19"
 murmur3 = "0.5.2"
+winapi = { version = "0.3.9", features = ["winbase", "processenv", "minwindef"] }
diff --git a/vm/src/execution_engine/system_native_table.rs b/vm/src/execution_engine/system_native_table.rs
index 9723e56f..1b2a8461 100644
--- a/vm/src/execution_engine/system_native_table.rs
+++ b/vm/src/execution_engine/system_native_table.rs
@@ -6,7 +6,7 @@ use crate::system_native::class::{
     class_init_class_name_wrp, for_name0_wrp, get_modifiers_wrp, get_primitive_class_wrp,
     is_array_wrp, is_interface_wrp, is_primitive_wrp,
 };
-use crate::system_native::file_descriptor::file_descriptor_close0_wrp;
+use crate::system_native::file_descriptor::{file_descriptor_close0_wrp, get_handle_wrp};
 use crate::system_native::file_output_stream::{
     file_output_stream_open0_wrp, file_output_stream_write_bytes_wrp, file_output_stream_write_wrp,
 };
@@ -181,7 +181,7 @@ static SYSTEM_NATIVE_TABLE: Lazy<HashMap<&'static str, NativeMethod>> = Lazy::ne
     table.insert("java/io/FileDescriptor:initIDs:()V", Basic(void_stub));
     table.insert(
         "java/io/FileDescriptor:getHandle:(I)J",
-        Basic(|args: &[i32]| return_argument_stub(&vec![0, args[0]])),
+        Basic(get_handle_wrp),
     );
     table.insert(
         "java/io/FileDescriptor:getAppend:(I)Z",
diff --git a/vm/src/system_native/file_descriptor.rs b/vm/src/system_native/file_descriptor.rs
index 0295066d..b10bd26b 100644
--- a/vm/src/system_native/file_descriptor.rs
+++ b/vm/src/system_native/file_descriptor.rs
@@ -1,20 +1,18 @@
-use crate::heap::heap::with_heap_read_lock;
-use std::fs::File;
-use std::os::fd::FromRawFd;
+use crate::helper::i64_to_vec;
+
+use crate::system_native::PlatformFile;
 
 pub(crate) fn file_descriptor_close0_wrp(args: &[i32]) -> crate::error::Result<Vec<i32>> {
     let fd_ref = args[0];
 
-    close0(fd_ref)?;
+    PlatformFile::close(fd_ref)?;
     Ok(vec![])
 }
 
-fn close0(fd_ref: i32) -> crate::error::Result<()> {
-    let raw_fd = with_heap_read_lock(|heap| {
-        heap.get_object_field_value(fd_ref, "java/io/FileDescriptor", "fd")
-    })?[0];
+pub(crate) fn get_handle_wrp(args: &[i32]) -> crate::error::Result<Vec<i32>> {
+    let fd = args[0];
+
+    let handle = PlatformFile::get_handle(fd)?;
 
-    let file = unsafe { File::from_raw_fd(raw_fd) };
-    drop(file);
-    Ok(())
+    Ok(i64_to_vec(handle))
 }
diff --git a/vm/src/system_native/file_output_stream.rs b/vm/src/system_native/file_output_stream.rs
index e41afbf6..7a8a8f22 100644
--- a/vm/src/system_native/file_output_stream.rs
+++ b/vm/src/system_native/file_output_stream.rs
@@ -1,9 +1,8 @@
-use crate::heap::heap::{with_heap_read_lock, with_heap_write_lock};
+use crate::heap::heap::with_heap_read_lock;
 use crate::system_native::string::get_utf8_string_by_ref;
+use crate::system_native::PlatformFile;
 use std::fs::OpenOptions;
 use std::io::Write;
-use std::mem::ManuallyDrop;
-use std::os::fd::{FromRawFd, IntoRawFd, RawFd};
 
 pub(crate) fn file_output_stream_open0_wrp(args: &[i32]) -> crate::error::Result<Vec<i32>> {
     let obj_ref = args[0];
@@ -16,18 +15,14 @@ pub(crate) fn file_output_stream_open0_wrp(args: &[i32]) -> crate::error::Result
 fn open0(obj_ref: i32, file_name_ref: i32, append: bool) -> crate::error::Result<()> {
     let file_name = get_utf8_string_by_ref(file_name_ref)?;
 
-    let posix_fd = open_file_raw(&file_name, append)?; // throw FileNotFoundException if file not found
-    let fd = posix_fd as i32;
-
-    with_heap_write_lock(|heap| {
-        let fd_ref = heap
-            .get_object_field_value(obj_ref, "java/io/FileOutputStream", "fd")
-            .expect("fd field not found")[0];
-        heap.set_object_field_value(fd_ref, "java/io/FileDescriptor", "fd", vec![fd])
-            .expect("fd field not found");
-    });
+    let file = OpenOptions::new() // throw FileNotFoundException if file not found
+        .write(true)
+        .create(true)
+        .truncate(!append)
+        .append(append)
+        .open(file_name)?;
 
-    Ok(())
+    Ok(PlatformFile::set_raw_id(obj_ref, file)?)
 }
 
 pub(crate) fn file_output_stream_write_wrp(args: &[i32]) -> crate::error::Result<Vec<i32>> {
@@ -38,9 +33,7 @@ pub(crate) fn file_output_stream_write_wrp(args: &[i32]) -> crate::error::Result
     Ok(vec![])
 }
 fn write(obj_ref: i32, byte: i32, _append: bool) -> crate::error::Result<()> {
-    let fd = extract_fd(obj_ref)?;
-
-    let mut file = ManuallyDrop::new(unsafe { std::fs::File::from_raw_fd(fd) }); // ManuallyDrop prevents `file` from being dropped
+    let mut file = PlatformFile::get_by_raw_id(obj_ref)?;
 
     write!(file, "{}", byte as u8 as char)?;
 
@@ -69,10 +62,7 @@ fn write_bytes(
     len: i32,
     _append: bool,
 ) -> crate::error::Result<()> {
-    let fd = extract_fd(obj_ref)?;
-
-    let mut file = ManuallyDrop::new(unsafe { std::fs::File::from_raw_fd(fd) }); // ManuallyDrop prevents `file` from being dropped
-
+    let mut file = PlatformFile::get_by_raw_id(obj_ref)?;
     let array = with_heap_read_lock(|heap| {
         heap.get_entire_array(bytes_ref)
             .expect("error getting array value")
@@ -87,22 +77,3 @@ fn write_bytes(
 
     Ok(())
 }
-
-fn extract_fd(obj_ref: i32) -> crate::error::Result<i32> {
-    with_heap_read_lock(|heap| {
-        let fd_ref = heap.get_object_field_value(obj_ref, "java/io/FileOutputStream", "fd")?[0];
-        let fd = heap.get_object_field_value(fd_ref, "java/io/FileDescriptor", "fd")?[0];
-
-        Ok(fd)
-    })
-}
-
-fn open_file_raw(file_name: &str, append: bool) -> std::io::Result<RawFd> {
-    let file = OpenOptions::new()
-        .write(true)
-        .create(true)
-        .truncate(!append)
-        .append(append)
-        .open(file_name)?;
-    Ok(file.into_raw_fd()) // move ownership out of file
-}
diff --git a/vm/src/system_native/mod.rs b/vm/src/system_native/mod.rs
index 1e847c76..d91811f1 100644
--- a/vm/src/system_native/mod.rs
+++ b/vm/src/system_native/mod.rs
@@ -8,3 +8,15 @@ pub(crate) mod system;
 pub(crate) mod system_props_raw;
 pub(crate) mod thread;
 pub(crate) mod unsafe_;
+
+//// Platform-specific file operations.
+#[cfg(unix)]
+mod unix_file;
+#[cfg(windows)]
+mod windows_file;
+
+#[cfg(unix)]
+pub use unix_file::PlatformFile;
+#[cfg(windows)]
+pub use windows_file::PlatformFile;
+////
diff --git a/vm/src/system_native/unix_file.rs b/vm/src/system_native/unix_file.rs
new file mode 100644
index 00000000..21daf99e
--- /dev/null
+++ b/vm/src/system_native/unix_file.rs
@@ -0,0 +1,52 @@
+use crate::error::Error;
+use crate::heap::heap::{with_heap_read_lock, with_heap_write_lock};
+use std::fs::File;
+use std::mem::ManuallyDrop;
+use std::os::fd::{FromRawFd, IntoRawFd};
+
+pub struct PlatformFile {}
+
+impl PlatformFile {
+    pub fn close(file_descriptor_ref: i32) -> crate::error::Result<()> {
+        let raw_fd = with_heap_read_lock(|heap| {
+            heap.get_object_field_value(file_descriptor_ref, "java/io/FileDescriptor", "fd")
+        })?[0];
+
+        let file = unsafe { File::from_raw_fd(raw_fd) };
+        drop(file);
+
+        Ok(())
+    }
+
+    pub fn get_handle(_fd: i32) -> crate::error::Result<i64> {
+        Ok(0)
+    }
+
+    pub fn set_raw_id(output_stream_ref: i32, file: File) -> std::io::Result<()> {
+        let posix_fd = file.into_raw_fd(); // move ownership out of file
+        let fd = posix_fd as i32;
+
+        with_heap_write_lock(|heap| {
+            let fd_ref = heap
+                .get_object_field_value(output_stream_ref, "java/io/FileOutputStream", "fd")
+                .expect("fd field not found")[0];
+            heap.set_object_field_value(fd_ref, "java/io/FileDescriptor", "fd", vec![fd])
+                .expect("fd field not found");
+        });
+
+        Ok(())
+    }
+
+    pub fn get_by_raw_id(obj_ref: i32) -> crate::error::Result<ManuallyDrop<File>> {
+        let fd = with_heap_read_lock(|heap| {
+            let fd_ref =
+                heap.get_object_field_value(obj_ref, "java/io/FileOutputStream", "fd")?[0];
+            let fd = heap.get_object_field_value(fd_ref, "java/io/FileDescriptor", "fd")?[0];
+
+            Ok::<i32, Error>(fd)
+        })?;
+
+        let file = ManuallyDrop::new(unsafe { std::fs::File::from_raw_fd(fd) }); // ManuallyDrop prevents `file` from being dropped
+        Ok(file)
+    }
+}
diff --git a/vm/src/system_native/windows_file.rs b/vm/src/system_native/windows_file.rs
new file mode 100644
index 00000000..638ee8e1
--- /dev/null
+++ b/vm/src/system_native/windows_file.rs
@@ -0,0 +1,81 @@
+use crate::error::Error;
+use crate::heap::heap::{with_heap_read_lock, with_heap_write_lock};
+use crate::helper::{i32toi64, i64_to_vec};
+use std::fs::File;
+use std::mem::ManuallyDrop;
+use std::os::windows::io::{FromRawHandle, IntoRawHandle, RawHandle};
+use winapi::shared::minwindef::DWORD;
+use winapi::um::processenv::GetStdHandle;
+use winapi::um::winbase::{STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE};
+
+pub struct PlatformFile {}
+
+impl PlatformFile {
+    pub fn close(file_descriptor_ref: i32) -> crate::error::Result<()> {
+        let raw_fd = with_heap_read_lock(|heap| {
+            let raw = heap.get_object_field_value(
+                file_descriptor_ref,
+                "java/io/FileDescriptor",
+                "handle",
+            )?;
+            Ok::<i64, Error>(i32toi64(raw[0], raw[1]))
+        })?;
+
+        eprintln!("close0: raw_fd={raw_fd}");
+
+        let file = unsafe { File::from_raw_handle(raw_fd as RawHandle) };
+        drop(file);
+        Ok(())
+    }
+
+    pub fn get_handle(fd: i32) -> crate::error::Result<i64> {
+        match fd {
+            0 => Self::get_std_handle(STD_INPUT_HANDLE),
+            1 => Self::get_std_handle(STD_OUTPUT_HANDLE),
+            2 => Self::get_std_handle(STD_ERROR_HANDLE),
+            _ => Err(Error::new_execution(&format!("fd {fd} is not supported"))),
+        }
+    }
+
+    fn get_std_handle(handle: DWORD) -> crate::error::Result<i64> {
+        unsafe {
+            let handle = GetStdHandle(handle);
+            if handle.is_null() {
+                return Err(Error::new_execution(&format!(
+                    "Failed to get handle by {handle:?}"
+                )));
+            }
+
+            Ok(handle as isize as i64)
+        }
+    }
+
+    pub fn set_raw_id(output_stream_ref: i32, file: File) -> std::io::Result<()> {
+        let raw_handle = file.into_raw_handle(); // move ownership out of file
+        let handle = raw_handle as isize as i64;
+        let raw = i64_to_vec(handle);
+
+        with_heap_write_lock(|heap| {
+            let fd_ref = heap
+                .get_object_field_value(output_stream_ref, "java/io/FileOutputStream", "fd")
+                .expect("fd field not found")[0];
+            heap.set_object_field_value(fd_ref, "java/io/FileDescriptor", "handle", raw)
+                .expect("handle field not found");
+        });
+
+        Ok(())
+    }
+
+    pub fn get_by_raw_id(obj_ref: i32) -> crate::error::Result<ManuallyDrop<File>> {
+        let handle = with_heap_read_lock(|heap| {
+            let fd_ref =
+                heap.get_object_field_value(obj_ref, "java/io/FileOutputStream", "fd")?[0];
+            let raw = heap.get_object_field_value(fd_ref, "java/io/FileDescriptor", "handle")?;
+
+            Ok::<i64, Error>(i32toi64(raw[0], raw[1]))
+        })?;
+
+        let file = ManuallyDrop::new(unsafe { File::from_raw_handle(handle as RawHandle) }); // ManuallyDrop prevents `file` from being dropped
+        Ok(file)
+    }
+}