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 port to Windows #6

Merged
merged 1 commit into from
Nov 4, 2020
Merged

Initial port to Windows #6

merged 1 commit into from
Nov 4, 2020

Conversation

compnerd
Copy link
Collaborator

@compnerd compnerd commented Oct 7, 2020

This adds enough shims and tweaks the types sufficiently to allow
building System on Windows.

@compnerd
Copy link
Collaborator Author

compnerd commented Oct 7, 2020

CC: @kylemacomber @milseman

Copy link
Contributor

@milseman milseman left a comment

Choose a reason for hiding this comment

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

Just some initial comments and questions. Glad to see you working on this!

Sources/System/WindowsPlatformConstants.swift Outdated Show resolved Hide resolved
public var system_errno: CInt {
get {
var value: CInt = 0
// TODO(compnerd) handle the error?
Copy link
Contributor

Choose a reason for hiding this comment

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

What is there to handle?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

_get_errno returns an errno_t. It can fail in case of a PEB/TEB corruption or the parameter being invalid.

@@ -36,47 +50,129 @@ public var system_errno: CInt {
#endif

public func system_open(_ path: UnsafePointer<CChar>, _ oflag: Int32) -> CInt {
#if os(Windows)
var fh: CInt = -1
_ = _sopen_s(&fh, path, oflag, _SH_DENYNO, _S_IREAD | _S_IWRITE)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the rough equivalent of POSIX open? E.g., what is the difference between a Windows file handle and a POSIX file descriptor?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, this is roughly the equivalent of the POSIX open.

I don't think that its valuable to try to enumerate every single difference. What type of differences are you really looking for?

But for a taste, there are a large number of differences between a file handle and a POSIX file. The POSIX file is a user mode only, process specific construct. It cannot be shared across processes. It is a mapping from the kernel side representation (HANDLE) and an application specific view. It is maintained by means of a lookup table in the application's address space by the C library. Additionally, it has a comparatively restricted set of operations available to it.

#if os(Windows)
public func system_open(
_ path: UnsafePointer<CChar>, _ oflag: Int32, _ mode: CInt
) -> CInt {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should try to unify the interface around trivial type casts

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I was considering that myself, but wasn't sure if you would be okay with choosing the minimal type across the interfaces.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If we want to do that, I think that doing a follow up might be the best way forward.

// TODO(compnerd) map windows error to errno
return Int(-1)
}
return Int(nNumberOfBytesRead)
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you explain more what is going on here? Does Windows not support pread/pwrite? We will have to decide on a case-by-case basis what should be handled by availability and what should be handled by implementing the functionality ourselves (but this doesn't look too bad). This is definitely worthy of being extracted into a helper function and given a good name (and comments!) since it's non-obvious to a non-Windows contributor what's going on.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, there is no pread or pwrite function that is available in the C library. However, the operation itself is supported, but you need to use the Win32 APIs to do that. Basically, I have to convert the file descriptor to the kernel/win32 representation (HANDLE) and then I can perform the operation via the Win32 API (ReadFile or WriteFile) as I don't see the value in going directly to the syscall interface (NtReadFile or NtWriteFile) as they are more complicated to use (it would directly map to the kernel call ZwReadFile or ZwWriteFile).

@jakepetroules
Copy link
Member

@compnerd How do you plan to handle file paths in SwiftSystem? You probably know what I'm referring to, but to spell it out for the record: 8-bit file paths on Windows are not UTF-8 like on POSIX (except on Windows 10 and even then only in certain cases). Should SwiftSystem go through the Win32 APIs instead, in order to ensure proper Unicode support?

@compnerd
Copy link
Collaborator Author

compnerd commented Oct 7, 2020

@jakepetroules thats something that I did bring up with @milseman and the plan is to have a representation which is platform dependent AIUI. So that would enable the paths to handle the proper conversion at the right time.

@milseman
Copy link
Contributor

milseman commented Oct 7, 2020

Right, so on nix you'd have an initializer taking a collection of bytes in some platform encoding (e.g. UTF-8) and on Windows you'd have an init taking (presumably) a collection of UInt16s. Both platforms can form paths from strings and both platforms can (with potential encoding error correction) go to a string. The storage representation of FilePath is hidden and opaque (even in the ABI).

return _lseek(fd, off, whence)
#else
return lseek(fd, off, whence)
#endif
Copy link
Contributor

Choose a reason for hiding this comment

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

To reduce the number of #if/#else inside of these functions, should Windows have a internal let lseek = _lseek, etc?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There is a lseek as well, so I don't think that we should be shadowing that.

@milseman
Copy link
Contributor

This LGTM after I merge mocking at #8. I apologize in advance that it will cause you some merge conflicts around the system_* functions. Hopefully we can get most of the #if/#else out of there.

@milseman
Copy link
Contributor

@compnerd sorry for the conflict, it's worth moving these sys call wrappers into their own file. The good news is that I was able to structure the mocking-related code such that there is less boilerplate and it all appears at the very top of the function, so your code shouldn't need much modification.

@compnerd
Copy link
Collaborator Author

That change introduced more than just conflicts :/ The threading model doesn't work for Windows - there is no pthread_t on Windows. I think that I can rebase the change, and we should merge it in the broken state, I'll redo the threading in a follow up.

@milseman
Copy link
Contributor

For Windows, would you just use TlsGet/SetValue? This isn't System API, it's in the internals, so it's just an implementation detail. We will definitely want to expose System API for TLS in the future, but that's further down the road and would need a more comprehensive design.

@milseman
Copy link
Contributor

If you can show me the Windows calls you'd like use and an example of how to use them, I can refactor the mocking code to accommodate Windows better.

@compnerd
Copy link
Collaborator Author

@milseman, @kylemacomber - this part is rebased, I'll do a follow up for the threading

@compnerd
Copy link
Collaborator Author

@milseman - lets discuss the threading in #9.

Copy link
Contributor

@milseman milseman left a comment

Choose a reason for hiding this comment

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

This LGTM, but I would like to do some cleanup. If there is something blocked by this, that can happen on a subsequent PR. If not, then it might be cleaner to do it here.

  1. Can Windows just define open, pwrite, lseek, etc? That would keep the system_foo bodies clean and straight forward. It's an unfortunate amount of boilerplate we have to have in System while we wait on compiler features (or we do codegen ourselves), so ideally the bodies would be simple and uniform.

  2. We'd like to unify the types for system_write and system_read interfaces as much as possible. I'm worried It might be too easy to break Windows when modifying the System module in what seems to be trivial changes. We can unify on the wider type.

return -1
case .counted(let e, let count):
assert(count >= 1)
#if os(Windows)
_ = ucrt._set_errno(e)
#else
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm definitely in favor of making a helper function to set errno if it means we can avoid scattering #if/#else throughout function bodies. But that can be a subsequent PR.

@compnerd
Copy link
Collaborator Author

  1. I tried to do that, but the you cannot do a typealias locally. I don't want to make it shadow the existing functions with the same name. Do you have another suggestion on how to do that? I suppose I can create sys... variants of the functions and use that.

  2. Why would we unify on the wider type? Doesn't that mean that some inputs become invalid? Shouldn't it be the narrower type?

@milseman
Copy link
Contributor

milseman commented Oct 22, 2020

One approach might be to add a WindowsSyscalls.swift that has something like:

#If os(Windows)
internal let lseek = _lseek
internal func open(...) {
  var handle: ...
  _open(&handle, ...)
  ...
  return handle
}
...
#endif

The advantage of this is it eliminates the #if in the bodies of the forwarding methods, which eventually will likely number in the hundreds (and we may end up using code generation for). This disadvantage is needing one more jump-to-definition hop when inspecting the source code. This also might be a good place to put the type widening/narrowing. WDYT?

@milseman
Copy link
Contributor

For "widening", what I mean is effectively moving the extending/truncating code from FileDescriptor.read into SystemInternals, so that instead of

// FileOperations.swift
extension FileDescriptor {
  func read(...) {
    Int(system_read(self.rawValue, buffer.baseAddress, numericCast(buffer.count)))
  }
}

We'd have:

// WindowsSyscalls.swift
#if os(Windows)
func read(_ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int) -> Int {
  Int(_read(fd, buf, numericCast(nbyte)))
}
#endif

being semantically equivalent and removing the cognitive burden from anyone touching other parts of System.

@milseman
Copy link
Contributor

I could take a stab at this if you like. I wouldn't be able to test it (or even compile it), but it might be a good proof of concept.

var fh: CInt = -1
_ = _sopen_s(&fh, path, oflag, _SH_DENYNO, _S_IREAD | _S_IWRITE)
return fh
}
Copy link
Contributor

Choose a reason for hiding this comment

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

mode is not used, so why do we care to type it as a CInt? For Windows, would we not have a permissions parameter for FileDescriptor.open?.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There is no mode_t type, and I don't think that you can use an internal typealias on a public func.

Copy link
Contributor

Choose a reason for hiding this comment

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

Should we make the FilePermissions OptionSet empty on Windows? I'm a little uneasy about ignoring information.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no, there is the concept of file permissions - its just that the permissions are different, there are no group permissions, and there are no execute permissions - only read/write.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, then we should we probably have group/execute unavailable on Windows, and the implementation of open here should do the read/write permission setting.

@milseman
Copy link
Contributor

#11 is a draft that shows what I mean, let me know what you think.

@compnerd
Copy link
Collaborator Author

Hmm, as long as we can ensure that the function gets flattened, I think that might not be a bad idea. I'll play around with that.

@milseman
Copy link
Contributor

Do you mean inlined? We can slap an @inline(__always) on them.

@compnerd
Copy link
Collaborator Author

Yeah, an @inline(__always) should do the trick - I was thinking of the clang flatten attribute which basically inlines all the way through a call stack, but the same idea.

@milseman
Copy link
Contributor

Alright, I think #11 is ready to review. It adds the basic organization and mocking support. If you want attribution for the code, feel free to steal the commits and use them for this branch :-). Up to you. Let me know if I can help with anything else to move this PR forwards.

@milseman
Copy link
Contributor

IIUC, the way to resolve the conflict is to remove all changes to Syscalls.swift, given that we now have WindowsSyscallAdapters.swift

Sources/System/Errno.swift Outdated Show resolved Hide resolved
Sources/SystemInternals/Syscalls.swift Outdated Show resolved Hide resolved
@@ -96,10 +98,10 @@ public func system_close(_ fd: Int32) -> Int32 {

// read
public func system_read(
_ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int
) -> Int {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we want the wider types here. Does Windows have a 64-bit offset API for read for 64-bit systems? For whatever reason, size_t is imported into Swift as Int

_ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int
) -> Int {
_ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: CUnsignedInt
) -> CInt {
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to above.

@@ -168,7 +168,7 @@ extension FileDescriptor {
retryOnInterrupt: Bool = true
) throws -> Result<Int, Errno> {
valueOrErrno(retryOnInterrupt: retryOnInterrupt) {
system_read(self.rawValue, buffer.baseAddress, buffer.count)
Int(system_read(self.rawValue, buffer.baseAddress, numericCast(buffer.count)))
Copy link
Contributor

Choose a reason for hiding this comment

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

Ideally, this casting would be done in the body of read/write inside the Windows syscall adapters. Can we avoid this at all?

This adds enough shims and tweaks the types sufficiently to allow
building System on Windows.
Copy link
Contributor

@milseman milseman left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks for all the cleanup!

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.

4 participants