Java/c#/powershell等各类语言在windows上调用com组件

使用场景
因为客户项目上需要在调用wincc的com组件, 拿到变量, 控制wincc弹窗, 所以有了这个需求.
一路调研下来, 使用了powershell, java, c#实现了调用过程, 加深了对这个30年前的老技术的了解, 记录于本篇.
com组件的介绍
COM(Component Object Model)技术是由微软开发的一种软件架构, 它在1990年代早期被引入.
COM 的设计目的是提供一种可以跨语言使用的面向对象的软件组件标准, 允许不同的应用程序或组件在同一个进程内或跨进程进行通信.
COM 成为了 Windows 操作系统中重要的技术基础, 支持了许多其他的技术, 如 OLE(Object Linking and Embedding), ActiveX 和后来的 .NET 框架.
COM是一种RPC技术, 通过windows注册表中的注册信息, 我们的com客户端可以调用com组件的host应用提供的API.
主要里程碑:
- 1993年: COM 首次作为 OLE 2.0 的一部分被引入. 这个版本的 OLE 主要用于文档的链接和嵌入, 如微软的 Word 和 Excel 这样的应用程序可以互相嵌入对象.
- 1996年: 随着互联网的兴起, COM 扩展为 ActiveX, 用于支持互联网浏览器中的交互式内容. ActiveX 允许网页嵌入可以在 Windows 系统上执行的小程序, 增强了网页的多媒体和交互性功能.
- 1990年代末: Distributed COM(DCOM)被引入, 它扩展了 COM 的能力, 允许不同计算机上的组件通过网络进行交互. 这是对 COM 进行的一种重要扩展, 使其可以支持分布式计算.
优点是协议标准, 支持不同语言的调用.
主要缺点是client和server必须处于同一个架构下(server是32位, client也必须是32位). 如果要调用32位的com服务, 写的应用也必须在32位下. 此外:
- 内存管理:COM使用引用计数来管理对象的生命周期,而.NET则使用垃圾回收。
- 类型系统:COM使用v-tables来实现接口,而.NET则使用更复杂的类型系统。
使用powershell(调试非常方便, 配合使用)
powershell使用com组件
关于与 COM (组件对象模型) 相关的操作, 以下是调试过程中的一些汇总, 主要用于过程调试, 还是很方便的.
作为一种RPC调用, com客户端的体系结构必须和server端一致,
所以我们也只能使用32位的powershell, 这个powershell的位置在:
%SystemRoot%\SysWOW64\WindowsPowerShell\v1.0\powershell.exe
- 创建 COM 对象 :
$h = New-Object -ComObject CCHMIRUNTIME.HMIRuntime: 通过此命令创建wincc运行时CCHMIRUNTIME.HMIRuntime的 COM 对象, 用于与 WinCC 运行时环境进行交互.
- 访问和操作屏幕元素:
$h.Screens["StartPicture"].ScreenItems["@ICM"].Visible = $true: 设置屏幕元素的可见性为真(显示). 此命令用于控制界面上的特定元素的显示状态.
- 读取和写入标签值:
$h.Tags["Z8U1A5000.S01"].Read(): 读取名为"Z8U1A5000.S01"的标签的当前值.$h.Tags["Z8U1A5000.S01"].Write(100): 将名为"Z8U1A5000.S01"的标签的值设置为 100.
- 处理 COM 对象的属性和方法:
$h.Screens["StartPicture"].ScreenItems["@ICM"].Visible: 获取名为"@ICM"的屏幕项在"StartPicture"屏幕上的可见性属性.$h.Tags["SIP.CW"].Write(1): 向名为"SIP.CW"的标签写入值1.
- 关闭和释放 COM 对象:
System.Runtime.InteropServices.Marshal]::ReleaseComObject($h): 释放已创建的 COM 对象, 以确保资源被正确清理, 避免内存泄漏.
这些操作展示了如何通过 PowerShell 脚本与 COM 对象交互, 实现对应用程序或设备的控制和数据管理. 这在自动化任务和集成外部系统或设备时特别有用.
调试中发现com读写的异常会导致powershell崩溃, 而且这个异常无法捕获.
此外, 操作Office组件还是非常方便的, 很有趣味.
# 打开word
$Word = New-Object -ComObject Word.Application
$Word.Visible = $true # 使 Word 窗口可见
# 添加文本到文档
$Selection = $Word.Selection
$Selection.TypeText("这是第一行文本")
$Selection.TypeParagraph() # 添加新的一段
$Selection.TypeText("这是第二行文本")
$Selection.TypeParagraph() # 添加新的一段
# 保存文档到特定目录
$Document.SaveAs("C:\Your\Specific\Directory\Document.docx")
# 关闭
$Document.Close()
$Word.Quit()
# 释放资源
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($Document) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($Word) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
Java生态中的com调用(clojure版, 调试的中间版本)
是一个有趣的探索过程, 但是也有很多限制, 特别是由于拿不到com的元数据, 不论是函数调用还是属性访问都没有提示, 需要不断手工尝试. 而且字节序问题需要解决.
- 只能是32位的jvm
- 使用jacbo这个库来实现
项目组织
- 增加项目对
jacob.jar的依赖 - jacob-1.21-x86.dll 放到项目的classpath, 保证可以引用到.
示例代码
读写tag
(ns clj-scheduler.wincc-variables (:require [taoensso.timbre :refer [error]]) (:import (com.jacob.activeX ActiveXComponent) (com.jacob.com Dispatch Variant))) (defn x86? [] (= "32" (System/getProperty "sun.arch.data.model"))) (defn create-wincc-instance' [] (if (x86?) (ActiveXComponent. "CCHMIRUNTIME.HMIRuntime") (error "当前系统64位, wincc执行不会执行"))) (def create-wincc-instance (memoize create-wincc-instance')) (defn get-tags' [wincc] (when wincc (.toDispatch (.getProperty wincc "Tags")))) (def get-tags (memoize get-tags')) (defn get-tag' [tags name] (when tags (.toDispatch (Dispatch/call tags "Item" (into-array String [name]))))) (def get-tag (memoize get-tag')) (defn read-tag [name] (some-> (create-wincc-instance) (get-tags) (get-tag name) (Dispatch/call "Read"))) (defn write-tag [name value] (some-> (create-wincc-instance) (get-tags) (get-tag name) (Dispatch/call "Write" (into-array [(Variant. value)])))) (defn- tag-value [name] (-> (create-wincc-instance) (get-tags) (get-tag name) (read-tag)))wincc控件交互
(ns clj-scheduler.wincc-utils "提供wincc对外操作的工具 1. phase对应弹窗的显示, 隐藏 2. phase命令的下发" (:require [clj-scheduler.wincc-variables :refer [read-tag write-tag create-wincc-instance]] [clojure.string :as str] [taoensso.timbre :refer [info error] :as timbre]) (:import [com.jacob.activeX ActiveXComponent] [com.jacob.com Dispatch Variant])) (defn get-activex-prop "获取属性, 作为activex控件返回" [parent prop] (.getPropertyAsComponent parent prop)) (defn get-activex-item "`parent`控件成员中的`index`对应的item, 作为active控件返回" [parent index] (when parent (ActiveXComponent. (.toDispatch (Dispatch/call parent "Item" (into-array [(Variant. index)])))))) (defn set-property "设置`activex-obj`控件的属性`prop`对应的`value`" [^ActiveXComponent activex-obj ^String prop value] (when activex-obj (.setProperty activex-obj prop (Variant. value)))) (defn get-property "获取`activex-obj`空间对应的`prop`的值" [^ActiveXComponent activex-obj prop] (.getPriority activex-obj prop)) (defn screen-item-icm "var PW = hmi.Screens[\"StartPicture\"].ScreenItems[\"@ICM\"]; 获取`@ICM`对应的activex组件" [] (some-> (create-wincc-instance) (get-activex-prop "Screens") (get-activex-item "StartPicture") (get-activex-prop "ScreenItems") (get-activex-item "@ICM"))) (defn show-phase-widget "显示ActiveX组件, 在行为上对应`phase-name`的弹窗" [tag phase-name] (let [;;var-name (str phase-name ".S01") ^ActiveXComponent icm (screen-item-icm)] (info (format "显示ActiveX组件, tag: %s, phase-name: %s" tag phase-name)) ;;(write-tag var-name 3) (.setProperty icm "Visible" true) (.setProperty icm "TagPrefix" tag) (.setProperty icm "ScreenName" "TPL_EPH_Overview") (.setProperty icm "CaptionText" phase-name) (write-tag "TPL_Device_description" phase-name) (write-tag "TPL_Device_tag" tag))) (defn hide-phase-widget "隐藏ActiveX组件, 在行为上对应`phase-name`的弹窗" [] (let [^ActiveXComponent icm (screen-item-icm)] (.setProperty icm "Visible" false)))
c#中的使用(最终在项目中使用的方案)
只能是32位应用, 必须手动指定项目的版本为x86, 即在
.csproj中设定<PlatformTarget>x86</PlatformTarget>dotnet5 以后其实并不能直接调用com组件, 需要自动或者手动的使用来生成 interop dll.
tlbimp.exe "c:\path\to\comlibrary\example.dll" /out:"c:\output\Interop.Example.dll"tlbimp 将元数据转换成等效的 .NET 元数据. 在此过程中, 它会生成类型库中定义的接口, 类, 枚举等类型的定义.
真正添加的依赖是
Interop.xxx.dll,
示例代码
CCHMIRUNTIME.HMIRuntime hmi = new CCHMIRUNTIME.HMIRuntime();
// Read
var value = hmi!.Tags[TagName].Read().ToString();
// Write
hmi.Tags[TagNmae].Write(value);
// 控制窗体
var icm = hmi.Screens["StartPicture"].ScreenItems["@ICM"];
// 动态属性必须使用dynamic
dynamic dicm = hmi.Screens["StartPicture"].ScreenItems["@ICM"];
// 静态属性
icm.Visible = true;
// 动态属性
dicm.TagPrefix = tag;
dicm.ScreenName = "TPL_EPH_Overview";
dicm.CaptionText = phaseName;
// ...
底层异常处理
在.NET 4.0之后, CLR将会区别出一些异常(都是SEH异常),
将这些异常标识为破坏性异常(Corrupted State Exception). 针对这些异常,
CLR的catch块不会捕捉这些异常. SEH异常通常是非托管代码抛出的. 例如:
调用c和c++的lib和dll库, 都是非托管的. 如果不做配置,
内存访问异常会导致程序崩溃, 调研发现在dotnet4 底层的异常捕获是关闭的,
配置在 app.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<legacyCorruptedStateExceptionsPolicy enabled="true"/>
</runtime>
</configuration>
最后, 我们项目中使用了c#的解决方案.