May 20, 2024
By: Kevin

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

  1. 使用场景
  2. com组件的介绍
  3. 使用powershell(调试非常方便, 配合使用)
    1. powershell使用com组件
  4. Java生态中的com调用(clojure版, 调试的中间版本)
    1. 项目组织
    2. 示例代码
  5. c#中的使用(最终在项目中使用的方案)
    1. 示例代码
    2. 底层异常处理
  6. 参考链接

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

  1. 创建 COM 对象 :
    • $h = New-Object -ComObject CCHMIRUNTIME.HMIRuntime: 通过此命令创建wincc运行时 CCHMIRUNTIME.HMIRuntime 的 COM 对象, 用于与 WinCC 运行时环境进行交互.
  2. 访问和操作屏幕元素:
    • $h.Screens["StartPicture"].ScreenItems["@ICM"].Visible = $true: 设置屏幕元素的可见性为真(显示). 此命令用于控制界面上的特定元素的显示状态.
  3. 读取和写入标签值:
    • $h.Tags["Z8U1A5000.S01"].Read(): 读取名为 "Z8U1A5000.S01" 的标签的当前值.
    • $h.Tags["Z8U1A5000.S01"].Write(100): 将名为 "Z8U1A5000.S01" 的标签的值设置为 100.
  4. 处理 COM 对象的属性和方法:
    • $h.Screens["StartPicture"].ScreenItems["@ICM"].Visible: 获取名为 "@ICM" 的屏幕项在 "StartPicture" 屏幕上的可见性属性.
    • $h.Tags["SIP.CW"].Write(1): 向名为 "SIP.CW" 的标签写入值 1.
  5. 关闭和释放 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的元数据, 不论是函数调用还是属性访问都没有提示, 需要不断手工尝试. 而且字节序问题需要解决.

  1. 只能是32位的jvm
  2. 使用jacbo这个库来实现

项目组织

  • 增加项目对 jacob.jar 的依赖
  • jacob-1.21-x86.dll 放到项目的classpath, 保证可以引用到.

示例代码

  1. 读写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)))
    
  2. 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#中的使用(最终在项目中使用的方案)

  1. 只能是32位应用, 必须手动指定项目的版本为x86, 即在 .csproj 中设定 <PlatformTarget>x86</PlatformTarget>

  2. 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#的解决方案.

参考链接

Tags: clojure java windows