从2个winapi调用返回的不一致错误

时间:2016-01-03 02:27:45

标签: winapi rust

这是我的源代码:

extern crate user32;
extern crate kernel32;

use std::io::prelude::*;
use std::net::TcpStream;
use std::ptr;

fn main() {
    let mut message = get_data();
    loop {
        if message != get_data() {
            let mut client = TcpStream::connect("127.0.0.1:8080").unwrap();
            message = get_data();
            println!("{}", message);
            let _ = client.write(message.as_bytes());
            println!("Sent!");
        }
    }
}

fn get_data() -> String {
    let value: String;
    unsafe {
        let open = user32::OpenClipboard(ptr::null_mut());
        if open == 0 {
            println!("{}", kernel32::GetLastError());
            user32::CloseClipboard();
            return "NULL".to_string()
        }
        let var = user32::GetClipboardData(13 as u32);
        if var.is_null() {
            println!("{}", kernel32::GetLastError());
            user32::CloseClipboard();
            return "NULL".to_string()
        }
        let data = kernel32::GlobalLock(var) as *mut u16;
        let len = rust_strlen16(data);
        let raws = std::slice::from_raw_parts(data, len);
        value = String::from_utf16_lossy(raws);
        kernel32::GlobalUnlock(var);
        user32::CloseClipboard();
    }
    value
}

#[inline(always)]                                       // used from clipboard-win, not mine
unsafe fn rust_strlen16(buff_p: *mut u16) -> usize {
    let mut i: isize = 0;
    while *buff_p.offset(i) != 0 {
        i += 1;
    }
    return i as usize
}

当我在message的第1行初始化main时,一切都很好,但是当我开始复制文本时,事情开始变得有些奇怪。

来自OpenClipboard中的get_data()函数,我将从GetLastError获得的错误为6(ERROR_INVALID_HANDLE)或5(ERROR_ACCESS_DENIED)一个未知的原因。

GetClipboardData函数我会始终从ERROR_CLIPBOARD_NOT_OPEN获得错误代码1418(GetLastError)。您会认为我从OpenClipboard获取错误代码6或5会收到此错误,但是它不能打印1418因为它无法在函数中走得那么远,对吧?我可能错了,但这就是我的看法。

你应该注意的是,在循环中比较message时会出现这些错误,但是if语句仍在继续,并且消息被分配了实际复制的数据,这里是一些示例输出

1418  // this is when message and get_data() are being checked
println!("{}", result); // this is the actual copied data which is message
Sent! // sent!
5
NULL
Sent!
println!("Got Connection");
Sent!

1 个答案:

答案 0 :(得分:3)

以下是关于发生了什么的猜测:系统强行关闭打开的剪贴板句柄。

据我所知,剪贴板是真正的全局共享资源,即使是跨进程也是如此。我无法追踪OpenClipboard函数的存在时间,但根据MSDN,它至少可以在Windows 2000上运行。如果它真的起源于Windows 98/95甚至在此之前,我不会感到惊讶。剪贴板是一个古老的概念!

然而,出现了一个概念性问题:如果一个程序打开剪贴板并忘记关闭它会怎么样?然后你的整个系统将无法使用剪贴板,这将是非常可怕的。我相信(通过没有证明)操作系统会跟踪您打开剪贴板的数量,并决定在您使用剪贴板时从中取回剪贴板。

运行程序时,您可以看到它与100%CPU挂钩,因为您构建了一个繁忙的循环,大部分时间都是剪贴板打开的。这足以触发一个简单的滥用启发式方法。将(::std::thread::sleep_ms(100))添加到循环的末尾似乎可以使一切行为更好。

但是,您仍会偶尔收到错误代码5(ERROR_ACCESS_DENIED)。我再次相信这是由剪贴板的全局共享性质引起的。某些其他进程由于某种原因打开了剪贴板而您无法拥有它。正确的做法是无法始终立即访问资源。

进行这些更改似乎可以使您的计划正常运行,但还有更多工作要做:

extern crate user32;
extern crate kernel32;

use std::ptr;
use std::marker::PhantomData;

fn main() {
    let mut sequence_val = None;

    loop {
        let clip = match Clipboard::open() {
            Err(WinApiError(ERROR_ACCESS_DENIED)) |
            Err(WinApiError(ERROR_CLIPBOARD_NOT_OPEN)) => {
                println!("Someone else is using the clipboard; continuing");
                continue;
            },
            Err(e) => panic!("Unknown error while opening the clipboard: {:?}", e),
            Ok(c) => c,
        };

        let next_sequence_val = Some(clip.sequence_number());
        if sequence_val != next_sequence_val {
            println!("Clipboard advanced from {:?} to {:?}", sequence_val, next_sequence_val);
            sequence_val = next_sequence_val;

            let all_formats: Result<Vec<_>, _> = clip.formats().collect();
            println!("Available formats: {:?}", all_formats);

            let message = clip.get_text().expect("Cannot read from clipboard");

            match message {
                Some(message) => println!("Got clipboard text: {}", message),
                None          => println!("Clipboard did not contain textual data"),
            }
        }

        ::std::thread::sleep_ms(250);
    }
}

#[derive(Debug, Copy, Clone)]
struct WinApiError(u32);

const ERROR_ACCESS_DENIED: u32 = 0x5;
const ERROR_CLIPBOARD_NOT_OPEN: u32 = 0x58A;

impl WinApiError {
    fn from_global() -> Result<(), WinApiError> {
        let val = unsafe { kernel32::GetLastError() };
        if val != 0 {
            Err(WinApiError(val))
        } else {
            Ok(())
        }
    }
}

// PhantomData is used here to prevent creating the struct with
// `Clipboard`.
struct Clipboard {
    marker: PhantomData<()>,
}

const CF_UNICODETEXT: u32 = 13;

impl Clipboard {
    fn open() -> Result<Clipboard, WinApiError> {
        unsafe {
            user32::OpenClipboard(ptr::null_mut());
            try!(WinApiError::from_global());
            Ok(Clipboard { marker: PhantomData })
        }
    }

    fn formats(&self) -> ClipboardFormats {
        ClipboardFormats { clip: PhantomData, fmt: 0 }
    }

    fn sequence_number(&self) -> u32 {
        unsafe { user32::GetClipboardSequenceNumber() }
    }

    fn get_text(&self) -> Result<Option<String>, WinApiError> {
        for fmt in self.formats() {
            let fmt = try!(fmt);
            if fmt == CF_UNICODETEXT {
                unsafe {
                    let var = user32::GetClipboardData(CF_UNICODETEXT);
                    try!(WinApiError::from_global());

                    // I don't believe this lock is actually
                    // needed. In searching around, it only seems to
                    // be used when you call `GlobalAlloc` and then
                    // set the clipboard with that data.

                    // If it's needed, consider making another RAII
                    // type to handle automatically unlocking.
                    let data = kernel32::GlobalLock(var) as *mut u16;
                    try!(WinApiError::from_global());

                    let len = rust_strlen16(data);
                    let raws = std::slice::from_raw_parts(data, len);
                    let value = String::from_utf16_lossy(raws);

                    kernel32::GlobalUnlock(var);
                    try!(WinApiError::from_global());

                    return Ok(Some(value));
                }
            }
        }

        Ok(None)
    }
}

impl Drop for Clipboard {
    fn drop(&mut self) {
        unsafe {
            // Ignore failure to close as we can't really do anything
            // about it
            user32::CloseClipboard();
            let _ = WinApiError::from_global();

        }
    }
}

unsafe fn rust_strlen16(buff_p: *mut u16) -> usize {
    let mut i = 0;
    while *buff_p.offset(i) != 0 { i += 1 }
    i as usize
}

struct ClipboardFormats<'a>{
    // PhantomData is used here to prevent outliving the opened
    // clipboard
    clip: PhantomData<&'a Clipboard>,
    fmt: u32,
}

impl<'a> Iterator for ClipboardFormats<'a> {
    type Item = Result<u32, WinApiError>;

    fn next(&mut self) -> Option<Self::Item> {
        let next_fmt = unsafe { user32::EnumClipboardFormats(self.fmt) };
        if next_fmt == 0 {
            match WinApiError::from_global() {
                Ok(_) => None,
                Err(e) => Some(Err(e)),
            }
        } else {
            self.fmt = next_fmt;
            Some(Ok(next_fmt))
        }
    }
}

即我创建了一个便捷类型WinApiError,它自动检查GetLastError并将其绑定到Rust标准Result类型。这与try!一起普遍使用。

我介绍了一个Clipboard结构,以便有一个实现Drop的类型。这使我们确信在完成剪贴板后总是关闭剪贴板。你可能想要添加一个close方法,如果你想早点关闭它,它会非常明显和简单:

fn close(self) {
    // Automatically dropped as it goes out of scope.
}

为了略微提高性能,我使用GetClipboardSequenceNumber返回剪贴板的整数时间戳。这允许进行更轻量级的检查以了解剪贴板是否已更改,而不是每次都创建String。它还会阻止您使用"NULL"的魔术字符串。您可能仍希望跟踪最后一个字符串,以了解用户是否再次复制了相同的文本。

从剪贴板中检索数据时,您应该遍历所有可用格式并对您关心的第一个采取操作。虽然您只关心UCS-2格式,但我在尝试调试时添加了这个格式,所以我想我就离开了。

有一个更好的解决方案,而不仅仅是我没有实施的睡眠。剪贴板更改时,AddClipboardFormatListener可以通知消息循环。这可能是正确的解决方案,但确实需要您创建一个窗口(即使它仅用于消息)。