2024年1月

这是一个系列文章《如何从零开始实现TDOA技术的 UWB 精确定位系统》第4部分。

重要提示(
劝退说明
):

Q:做这个定位系统需要基础么?
A:文章不是写给小白看的,需要有电子技术和软件编程的基础
Q:你的这些硬件/软件是开源的吗?
A:不是开源的。这一系列文章是授人以“渔”,而不是授人以“鱼”。文章中我会介绍怎么实现UWB定位系统,告诉你如何克服难点,但不会直接把PCB的Gerber文件给你去做板子,不会把软件的源代码给你,不会把编译好的固件给你。我不会给你任何直接的结果,我只是告诉你方法。
Q:我个人对UWB定位很兴趣,可不可以做出一个定位系统?
A:如果是有很强的硬件/软件背景,并且有大量的时间,当然可以做得出来。文章就是写给你看的!
Q:我是商业公司,我想把UWB定位系统搞成一个商业产品。
A:当然可以。这文章也是写给你看的。如果你想自己从头构建整个系统,看了我的文章后,只需要画电路打板;构思软件结构再编码。就这样,所有的难点我都会在文中提到,并介绍了解决方法。你不需要招人来做算法研究。如果你想省事省时间,可以直接购买我们的电路图(AD工程文件),购买我们的软件源代码,然后快速进入生产环节。(网站:
https://uwbhome.top

标签的固件设计

之前的文章,我介绍了定位基站和标签的硬件设计、基站的固件设计(包括时钟同步的算法原理)。按正常的 roadmap,接下来应该是标签的固件设计。但是标签的固件实在是简单,没多少内容可写啊。想了想,还是为它写一篇吧。

标签最重要的功能是定期发送TDOA定位数据包,其他的功能都属于附加的。这个功能实现起来太简单了,decawave提供的样例代码中,很多都可以发送uwb数据包,只是数据包的格式需要符合我们的定义。

我们定义的 UWB TDOA定位数据包的格式是这样:

// Tag测距帧
// 广播帧, Tag 发出的测距消息, 各个 Anchor 根据收到这个帧的时间差来应用 TDOA 测距
typedef struct ieee154_broadcast_tag_range_frame {
	uint8_t frameCtrl[2];			//  0, 2: frame control bytes 00-01: 0x01 (Frame Type 0x01=date), 0xC8 (0xC0=src extended address 64 bits, 0x08=dest address 16 bits)
	uint8_t seq8;				//  2, 1: sequence_number 02
	uint8_t panID[2];			//  3, 2: PAN ID 03-04
	uint8_t destAddr[2];			//  5, 2: 0xFFFF
	uint8_t sourceAddr[8];			//  7, 8: 64Bits EUI地址
	uint8_t	messageType;			// 15, 1: Message Type = RTLS_MSG_TYPE_TAG_RANGE
	uint8_t	seq64[8];			// 16, 8: Tag 发出的测距消息序号, 比 seq8 有更在的最大值
	uint8_t powerVoltage[2];		// 24, 2: 电源电压*1000
	uint8_t batteryVoltage[2];		// 26, 2: 电池电压*1000
	uint8_t lighteness[2];			// 28, 2: 亮度, 直接 ADC 转换过来的值
	uint8_t switchStatus;			// 30, 1: 开关状态
	int16_t imu_aacx;			// 31, 2:
	int16_t imu_aacy;			// 33, 2:
	int16_t imu_aacz;			// 35, 2:
	int32_t imu_roll;			// 37, 4:
	int32_t imu_pitch;			// 41, 4:
	int32_t imu_yaw;			// 45, 4:
	int16_t imu_temp;			// 49, 2:
	uint8_t fcs[2];				// 51, 2
} BROADCAST_TAG_RANGE_MESSAGE;

后来我们发现这个数据包太大了,有很多字段是没有必要的,所以换了一种小的格式:

// Tag测距帧, 最小测距帧
// 广播帧, Tag 发出的测距消息, 各个 Anchor 根据收到这个帧的时间差来应用 TDOA 测距
typedef struct ieee154_broadcast_tag_range_min_frame_v2 {
	uint8_t frameCtrl[2];			//  0, 2: frame control bytes 00-01: 0x01 (Frame Type 0x01=date), 0xC8 (0xC0=src extended address 64 bits, 0x08=dest address 16 bits)
	uint8_t seq8;				//  2, 1: sequence_number 02
	uint8_t panID[2];			//  3, 2: PAN ID 03-04
	uint8_t destAddr[2];			//  5, 2: 0xFFFF
	uint8_t sourceAddr[8];			//  7, 8: 64Bits EUI地址
	uint8_t	messageType;			// 15, 1: Message Type = RTLS_MSG_TYPE_TAG_MIN_RANGE_V2
	uint8_t	seq32_3[3];			// 16, 3: Tag 发出的测距消息序号的高3字节, 与 seq8 组合为 seq32, 比 seq8 有更在的最大值
	uint8_t switchStatus;			// 19, 1: 开关状态
	uint8_t fcs[2];				// 20, we allow space for the CRC as it is logically part of the message. However ScenSor TX calculates and adds these bytes.
} BROADCAST_TAG_RANGE_MIN_MESSAGE_V2;		// 以上合计 22 字节

从53字节减小到22字节。这样,一个定位数据包在发送期间占用频率的时间减小了42%。这意味着可以容纳更多的标签。

你应该注意到,最小化的这个数据包中甚至没有包含电压数据,我们另外定义了一个包含电压数据的定位数据包,隔一段时间定期发送,因为电池电压不会快速变化,几分钟发一个带电压信息的包就可以了。

有客户定制了带心率传感器的标签,我们还增加了一个包含心率、血氧数据的定位数据包。

标签不像基站可以通过网络通讯,标签只能使用USB通讯。USB接口使用HID方式,可以免驱动。不要使用模拟COM口,因为如果使用COM通讯,一方面是需要安装驱动,另一方面是有可能会出现COM冲突,总之多出一些麻烦事。而使用HID方式则简单得多。

USB通讯需要使用外部晶振,不能使用STM32的内置RC振荡器。因为RC振荡器的频率不是很准确,如果使用RC振荡器,可能会导致兼容性问题,在一些电脑上因为频率不准而导致通讯失败。

STM32的省电模式使用 STOP 模式,这个模式比较适中。

要注意STM32的时钟频率。正常情况下,STM32的时钟是72MHz,因为我们不需要那么高的运算能力,所以使用48MHz就可以了,频率低一些可以省电。其他的更低频率不行,因为我们还需要给USB部分提供12MHz的频率,更低的频率组合不出12MHz。

还有就是,这么简单的程序不需要跑OS,直接裸奔就可以了。不跑OS还有两个原因:

  1. RTOS的结构复杂,我们还需要进行入STOP状态,这导致程序逻辑会复杂得多。
  2. STM32F103CBT6这个芯片的RAM/Flash都比较小,不适合太大的程序。

有什么想法,欢迎给我留言、评论。

这篇就这样吧,只差直接给出源代码了。下一篇开始介绍TDOA算法和定位引擎。

顺便做个找工作的广告。因为经营上的原因,公司散伙了。本人30+年工作经验,C/C++/Java/Delphi有20+年经验,Javascript/Python/Lua会一点。199x年x86汇编写过汉字系统,徒手Delphi写过邮件服务器,写过的应用系统无数,写过的代码应该超过200万行。10+年硬件设计经验,设计过多款嵌入式产品。这个UWB定位系统在初期,硬件软件都是我一个人弄出来的,产品成型之后才增加人手组团队。Base贵阳,或远程。如果有工作机会,请联系我要详细的简历。

领导:为什么每次项目部署后,有的用户要清缓存才能看到最新的页面

我:浏览器有默认的缓存策略,如果服务器在响应头中没有禁用缓存,那么浏览器每次请求页面会先看看缓存里面有没有,有的话从缓存取,造成还是取的旧页面。正常来说,用户只需要点击刷新按钮,刷新一下页面就好了,不必清除浏览器缓存刷新。

领导:为什么缓存这么严重,有的用户清除缓存刷新还是不行,关掉浏览器重新进来还是不行,要重启电脑才有效。

我:要重启电脑?这 。。。。。。用户都这样么,还是只有一小部分用户。

领导:不是所有的用户,有个别用户会出现这种情况

我:那可能得到用户电脑上看看了

每次需求投产后,因为有缓存问题导致用户看到的还是旧版内容,使用过程中出现了问题,联系我们才知道项目更新了,用户体验不好;

于是查找资料,寻找合适的方案,根据
评论区
的讨论,实践总结了下面
3
种前端部署后页面检测版本更新的方法

当检测到版本更新则及时通知用户,用户可以选择是否立即更新,并不会影响用户当前进行的业务;

下面以 vue 项目为例

1、轮询打包后的 index.html,比较生成的 js 文件的 hash

项目打包后,index.html 会包含打包后的 js 文件,这些文件的文件名包含的 hash 将会和上一次打包的不同,比较 hash 也就能判断是否有版本更新;

let firstV = [] //记录初始获得的 script 文件字符串
let currentv = [] //记录当前获得的 script 文件字符串

// 获得的文件字符串类似这样 `<script src="/js/chunk-vendors.1234fff.js"></script>`

async function getHtml() {
let res = await axios.get('/index.html?date=' + Date.now())
    if (res.status == '200') {
        let text = res.data
        if (text) {
            // 解析 html 内容,匹配 script 字符串
            let reg = /<script([^>]+)><\/script>/ig
            return text.match(reg) 
        }
    }
    return []
}
function isEqual(a, b) {
    return a.length = Array.from(new Set(a.concat(b))).length
}

export async function checkIfNewVersion() {

    firstV = await getHtml()

    window.checkVersionInterval && clearInterval(window.checkVersionInterval)

    window.checkVersionInterval = setInterval(async () =>{

        currentV = await getHtml()
        console.log(firstV,currentv)
        // 当前 script hash 和初始的不同时,说明已经更新
        if(!isEqual(firstV, currentv)) {
            console.log('已更新')
        }
    },3000)
}

// 文档可见时检测版本是否更新
document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "visible") {
    checkIfNewVersion();
  } else {
    window.checkVersionInterval && clearInterval(window.checkVersionInterval)
  }
});

getHtml()
得到的结果示例如下:

[
    '<script src="/js/chunk-vendors.1234fff.js"></script>',
    '<script src="/js/app.1234fff.js"></script>',
]

改动了一点业务代码后,再次打包,上面 app.js 的 hash 就会发生变化

[
    '<script src="/js/chunk-vendors.1234fff.js"></script>',
    '<script src="/js/app.12ed5ca.js"></script>',
]

比较两个的结果,如果结果不一样,则代表有版本更新。

2、HEAD 方法轮询响应头中的
etag

ETag
是资源的特定版本的标识符。当资源内容发生变化时,会生成新的
ETag

HEAD
方法请求资源的响应头信息,服务器不会返回响应体,可以节省带宽资源;

1.png

这里可以轮询打包后的 index.html,取两次响应头中的
eTag
比较,如果不同,说明版本更新了;
前提是服务器没有禁用缓存。

let firstEtag = `` //记录第一次进来请求获得的 etag
let currentEtag = `` //记录当前的 etag,会不断的刷新

async function getEtag(){
    let res = await axios.head('/index.html')
    if(res.status == '200'){
        if(res.headers && res.headers.etag){
            return res.headers.etag
        }
    }
    return ''
}

export async function checkEtag() {

    firstEtag = await getEtag()

    window.checkEtagInterval && clearInterval(window.checkEtagInterval)

    window.checkEtagInterval = setInterval(async() =>{
        // 每隔一定时间请求最新的 etag
        currentEtag = await getEtag()
        // 当前最新的 currentEtag 和初始 firstEtag 进行比较,不同则说明资源更新了;
        if(firstEtag && currentEtag && firstEtag!==currentEtag){
            console.log('已更新')
        }
    },3000)
}

// 文档可见时检测版本是否更新
document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "visible") {
    checkEtag();
  } else {
    window.checkEtagInterval && clearInterval(window.checkEtagInterval)
  }
});

3、监听
git commit hash
变化

项目改动提交 git 时会生成唯一的 hash 字符串,将最近提交的
commit hash
作为版本号保存在一个 json 文件中;通过轮询 json 文件,检测里面的版本号是否和上次不同,不同则表示有版本更新;

监听
git commit hash
变化的好处是只要投产的版本有 git 提交记录,
而不管静态文件变化还是代码变化
,都能检测到版本更新;

在 vue.config.js 中引入
git-revision-webpack-plugin
,该插件可获取到项目本地 git 的最新提交 commit hash

const GitRevisionPlugin  = require('git-revision-webpack-plugin')
const gitRevision = new GitRevisionPlugin()

const { writeFile , existsSync } = require('fs')
if(existsSync('./public')){
    fs.writeFile(
        './public/version.json', 
        `{"commitHash":${JSON.stringify(gitRevision.commithash())}`, 
        (error) =>{}
    )
}

上面代码使用
gitRevision.commithash()
获取
commit hash
,将其存入到
public/versionHash.json
文件中;

项目打包会执行上面的代码,生成后的
'versionHash.json'
文件类似这样

// 示例
{ "commitHash" : "234fjsdr322f32f322f32f3g32g23jglk32gjkl32lg3" }

项目改动后,提交改动的地方后,再次打包,会将最新的
commit hash
存入到
public/versionHash.json

// 示例
{ "commitHash" : "234fjsdr322f3eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" }

然后在页面中轮询
'/versionHash.json'
,比较
commit hash
,检测是否有更新

let firstCommitHash = ``
let currentCommitHash = ``

async function getCommitHash() {
    // 避免浏览器缓存加上时间戳参数
    let res = await axios.get('/versionHash.json?date=' + Date.now())
    if (res.status == '200') {
        if (res.data && res.data.commitHash) {
            return res.data.commitHash
        }
    }
    return ''
}

export async function checkCommitHash() {

    firstCommitHash = await getCommitHash()

    window.checkCommitHash && clearInterval(window.checkCommitHash)

    window.checkCommitHash = setInterval(async () => {
        // 轮询 versionHash.json 文件
        currentCommitHash = await getCommitHash()

        if (firstCommitHash && currentCommitHash && firstCommitHash !== currentCommitHash) {

            console.log('已更新')
            // 作相应处理
        }

    }, 3000)
}

关于检测版本更新的时机

检测时机,我觉得有三种比较合适,可以灵活搭配上面的方法使用

  • 资源加载错误时(常常发生在切换菜单时),检测版本更新
  • 路由切换发生错误时(也发生在切换菜单时或者当前页面引用其他路由时),检测版本更新
  • 监听
    visibilitychange + focus
    事件
1、资源加载错误时

前端部署后,某些资源已经更新,当切换菜单时,可能会出现资源加载失败的错误(404)。此时可以使用
addEventListener('error')
捕获资源加载错误

window.addEventListener('error',(event) =>{
    // 检测版本更新
    // window.location.reload()
},true)
2、路由切换发生错误时

和上面的
addEventListener('error')
捕获资源加载错误类似,
vue-router

router.onError()
方法可以捕获到路由加载的错误。

路由切换时某些资源加载失败,会抛出
Loading chunk chunk-xxxx failed
,可以用正则匹配它并作相应处理;

router.onError((error) =>{
    let reg = /Loading.*?failed/g
    if(reg.test(error)){
        // 检测版本更新
        // window.location.reload()
    }
})
3、监听
visibilitychange + focus
事件

visibilitychange
:当其选项卡的内容变得可见或被隐藏时,会在 document 上触发
visibilitychange
事件。

当用户导航到新页面、切换标签页、关闭标签页、最小化或关闭浏览器,或者在移动设备上从浏览器切换到不同的应用程序时,该事件就会触发,其
visibilityState

hidden

在 pc 端,从浏览器切换到其他应用程序并不会触发
visibilitychange
事件,所以加以
focus
辅佐;
当鼠标点击过当前页面
(必须 focus 过),此时切换到其他应用会触发页面的
blur
实践;再次切回到浏览器则会触发
focus
事件;

document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "visible") {
        
        // 开始检测更新
    } else {
        
        // 结束检测更新
    }
});

document.addEventListener('focus',() =>{

    // 开始检测更新
})

关于禁用缓存

禁用 html 缓存
<!-- HTTP/1.1 -->
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">

<!-- HTTP/1.0; 与 Cache-Control: no-cache 效果一致 -->
<meta http-equiv="Pragma" content="no-cache"> 

<!-- 如果在 Cache-Control 设置了 "max-age" 或者 "s-max-age" 指令,那么 `Expires` 头会被忽略。-->
<meta http-equiv="Expires" content="0">

如果只在 html 中设置这个的话,只在 IE 中有效;若要在其他浏览器中生效,则需要对服务器设置禁用缓存;

nginx 设置禁用缓存
// 配置 html 和 htm 文件不缓存
location / {
    root   html;
    index  index.html index.htm;
    add_header Cache-Control "no-cache,no-store,must-revalidate";
}

总结

本文总结了 3 种前端部署后页面检测版本更新的方法;

  • 轮询打包后的 index.html,比较生成的 js 文件的 hash
  • HEAD 方法轮询响应头中的
    etag
  • 监听
    git commit hash
    变化

3 种都有用武之地,看具体场景和需求;

监听
git commit hash
变化优势是可以检测到静态资源的变化;

HEAD 方法轮询响应头中的
etag
,优势是只需要取响应头中的字段,服务器不需要返回响应体,节约资源;

原文链接:
https://gaoyubo.cn/blogs/f57f32cf.html

前置

Golang实现JAVA虚拟机-解析class文件

Golang实现JAVA虚拟机-运行时数据区

一、字节码、class文件、指令集的关系

class文件(二进制)和字节码(十六进制)的关系

class文件

  • 经过编译器编译后的文件(如javac),一个class文件代表一个类或者接口;

  • 是由字节码组成的,主要存储的是字节码,字节码是访问jvm的重要指令

  • 文件本身是2进制,对应的是16进制的数。

字节码

  • 包括
    操作码(Opcode)

    操作数
    :操作码是一个字节

  • 如果方法不是抽象的,也不是本地方法,方法的Java代码就会被编译器编译成字节码,存放在method_info结构的Code属性中


如图:操作码为B2,助记符为助记符是
getstatic
。它的操作数是0x0002,代表常量池里的第二个常量。

操作数栈和局部变量表只存放数据的值, 并不记录数据类型。
结果就是:指令必须知道自己在操作什么类型的数据。

这一点也直接反映在了操作码的
助记符
上。

例如,iadd指令:对int值进行加法操作;
dstore指令:把操作数栈顶的double值弹出,存储到局部变量表中;
areturn:从方法中返回引用值。

助记符

如果某类指令可以操作不同类型的变量,则助记符的第一个字母表示变量类型。助记符首字母和变量类型的对应关系如下:

指令分类

Java虚拟机规范把已经定义的205条指令按用途分成了11类, 分别是:

  • 常量(constants)指令
  • 加载(loads)指令
  • 存储(stores)指令
  • 操作数栈(stack)指令
  • 数学(math)指令
  • 转换(conversions)指令
  • 比较(comparisons)指令
  • 控制(control)指令
  • 引用(references)指令
  • 扩展(extended)指令
  • 保留(reserved)指令:
    • 操作码:202(0xCA),助记符:breakpoint,用于调试器的断点调试
    • 254(0xFE),助记符:impdep1
    • 266(0xFF),助记符:impdep2
    • 这三条指令不允许出现在class文件中

本章将要实现的指令涉及11类中的9类

二、JVM执行引擎

执行引擎是Java虚拟机四大组成部分中一个核心组成(另外三个分别是
类加载器子系统

运行时数据区

垃圾回收器
),

Java虚拟机的执行引擎主要是用来执行Java字节码。

它有两种主要执行方式:通过
字节码解释器
执行,通过
即时编译器
执行

解释和编译

在了解字节码解释器和即使编译器之前,需要先了解
解释

编译

  • 解释是将代码逐行或逐条指令地转换为机器代码并立即执行的方式,适合实现跨平台性。
  • 编译是将整个程序或代码块翻译成机器代码的方式,生成的机器代码可反复执行,通常更快,但不具备跨平台性。

字节码解释器

字节码解释器将逐条解释执行Java字节码指令。这意味着它会逐个读取字节码文件中的指令,并根据每个指令执行相应的操作。虽然解释执行相对较慢。

逐行解释和执行代码。它会逐行读取源代码或字节码,将每一行翻译成计算机指令,然后立即执行该指令。

因此具有平台无关性,因为字节码可以在不同的平台上运行。

即时编译器(Just-In-Time Compiler,JIT)

即时编译器将字节码编译成本地机器代码,然后执行本地代码。

这种方式更快,因为它避免了字节码解释的过程,但编译需要一些时间。

即时编译器通常会选择性地编译某些热点代码路径,以提高性能。

解释器规范

Java虚拟机规范的2.11节介绍了Java虚拟机解释器的大致逻辑,如下所示:

do {
    atomically calculate pc and fetch opcode at pc;
    if (operands) fetch operands;
    execute the action for the opcode;
} while (there is more to do);
  1. 从当前程序计数器(Program Counter,通常简称为 PC)中获取当前要执行的字节码指令的地址。
  2. 从该地址获取字节码指令的操作码(opcode),并执行该操作码对应的操作。
  3. 如果指令需要操作数(operands),则获取操作数。
  4. 执行指令对应的操作。
  5. 更新 PC,以便继续执行下一条字节码指令。
  6. 循环执行上述步骤,直到没有更多的指令需要执行。


每次循环都包含三个部分:计算pc、指令解码、指令执行

可以把这个逻辑用Go语言写成一个for循环,里面是个大大的
switch-case
语句。但这样的话,代码的可读性将非常差。

所以采用另外一种方式:把指令抽象成接口,解码和执行逻辑写在具体的指令实现中。

这样编写出的解释器就和Java虚拟机规范里的伪代码一样简单,伪代码如下:

for {
    pc := calculatePC()
    opcode := bytecode[pc]
    inst := createInst(opcode)
    inst.fetchOperands(bytecode)
    inst.execute()
}

三、指令和指令解码

本节先定义指令接口,然后定义一个结构体用来辅助指令解码

Instruction接口

为了便于管理,把每种指令的源文件都放在各自的包里,所有指令都共用的代码则放在
base包
里。

因此
instructions目录
下会有如下10个子目录:

base目录
下创建
instruction.go
文件,在其中定义
Instruction接口
,代码如下:

type Instruction interface {
    FetchOperands(reader *BytecodeReader)
    Execute(frame *rtda.Frame)
}

FetchOperands()
方法从字节码中提取操作数,
Execute()
方法执行指令逻辑。

有很多指令的操作数都是类似的。为了避免重复代码,按照操作数类型定义一些结构体,并实现
FetchOperands()
方 法。

无操作数指令


instruction.go
文件中定义
NoOperandsInstruction
结构体,代码如下:

type NoOperandsInstruction struct {}

NoOperandsInstruction
表示没有操作数的指令,所以没有定义 任何字段。
FetchOperands()
方法自然也是空空如也,什么也不用 读,代码如下:

func (self *NoOperandsInstruction) FetchOperands(reader *BytecodeReader) {
	// nothing to do
}

跳转指令

定义
BranchInstruction结构体
,代码如下:

type BranchInstruction struct {
    //偏移量
	Offset int
}

BranchInstruction
表示跳转指令,
Offset字段
存放跳转偏移量。

FetchOperands()
方法从字节码中读取一个
uint16
整数,转成int后赋给
Offset字段
。代码如下:

func (self *BranchInstruction) FetchOperands(reader *BytecodeReader) {
	self.Offset = int(reader.ReadInt16())
}

存储和加载指令

存储和加载类指令需要根据索引存取局部变量表,索引由单字节操作数给出。把这类指令抽象成
Index8Instruction结构体
,定义
Index8Instruction结构体
,代码如下:

type Index8Instruction struct {
    //索引
    Index uint
}

FetchOperands()
方法从字节码中读取一个int8整数,转成
uint
后赋给
Index字段
。代码如下:

func (self *Index8Instruction) FetchOperands(reader *BytecodeReader) {
	self.Index = uint(reader.ReadUint8())
}

访问常量池的指令

有一些指令需要访问运行时常量池,
常量池索引
由两字节操作数给出,用
Index字段
表示常量池索引。定义
Index16Instruction结构体
,代码如下:

type Index16Instruction struct {
	Index uint
}

FetchOperands()
方法从字节码中读取一个
uint16
整数,转成
uint
后赋给
Index字段
。代码如下

func (self *Index16Instruction) FetchOperands(reader *BytecodeReader) {
    self.Index = uint(reader.ReadUint16())
}

指令接口和“抽象”指令定义好了,下面来看
BytecodeReader结构体

BytecodeReader结构体

base目录
下创建
bytecode_reader.go
文件,在 其中定义
BytecodeReader结构体

type BytecodeReader struct {
    code []byte // bytecodes
    pc   int
}

code字段
存放字节码,pc字段记录读取到了哪个字节。

为了避免每次解码指令都新创建一个
BytecodeReader
实例,给它定义一个
Reset()
方法,代码如下:

func (self *BytecodeReader) Reset(code []byte, pc int) {
    self.code = code
    self.pc = pc
}

面实现一系列的
Read()
方法。首先是最简单的
ReadUint8()
方法,代码如下:

func (self *BytecodeReader) ReadUint8() uint8 {
    i := self.code[self.pc]
    self.pc++
    return i
}

  • self.code
    字节切片中的
    self.pc
    位置读取一个字节(8 位)的整数值。
  • 然后将
    self.pc
    的值增加1,以便下次读取下一个字节。
  • 最后,返回读取的字节作为无符号 8 位整数

ReadInt8()
方法调用
ReadUint8()
,然后把读取到的值转成
int8
返回,代码如下:

func (self *BytecodeReader) ReadInt8() int8 {
	return int8(self.ReadUint8())
}

ReadUint16()
连续读取两字节

func (self *BytecodeReader) ReadUint16() uint16 {
    byte1 := uint16(self.ReadUint8())
    byte2 := uint16(self.ReadUint8())
    return (byte1 << 8) | byte2
}

ReadInt16()
方法调用
ReadUint16()
,然后把读取到的值转成
int16
返回,代码如下:

func (self *BytecodeReader) ReadInt16() int16 {
	return int16(self.ReadUint16())
}

ReadInt32()
方法连续读取4字节,代码如下:

func (self *BytecodeReader) ReadInt32() int32 {
    byte1 := int32(self.ReadUint8())
    byte2 := int32(self.ReadUint8())
    byte3 := int32(self.ReadUint8())
    byte4 := int32(self.ReadUint8())
    return (byte1 << 24) | (byte2 << 16) | (byte3 << 8) | byte4
}

在接下来的小节中,将按照分类依次实现约150条指令,占整个指令集的3/4

四、常量指令

常量指令把常量推入操作数栈顶。

常量可以来自三个地方:隐含在
操作码里

操作数

运行时常量池

常量指令共有21条,本节实现其中的18条。另外3条是
ldc
系列指令,用于从运行时常量池中加载常量,将在后续实现。

nop指令

nop指令
是最简单的一条指令,因为它什么也不做。

\instructions\constants
目录下创建
nop.go
文件,在其中实现nop指令,代码如下:

type NOP struct{ base.NoOperandsInstruction }

func (self *NOP) Execute(frame *rtda.Frame) {
// 什么也不用做
}

const系列指令

这一系列指令把
隐含在操作码中的常量值
推入操作数栈顶。

constants
目录下创建
const.go
文件,在其中定义15条指令,代码如下

type ACONST_NULL struct{ base.NoOperandsInstruction }
type DCONST_0 struct{ base.NoOperandsInstruction }
type DCONST_1 struct{ base.NoOperandsInstruction }
type FCONST_0 struct{ base.NoOperandsInstruction }
type FCONST_1 struct{ base.NoOperandsInstruction }
type FCONST_2 struct{ base.NoOperandsInstruction }
type ICONST_M1 struct{ base.NoOperandsInstruction }
type ICONST_0 struct{ base.NoOperandsInstruction }
type ICONST_1 struct{ base.NoOperandsInstruction }
type ICONST_2 struct{ base.NoOperandsInstruction }
type ICONST_3 struct{ base.NoOperandsInstruction }
type ICONST_4 struct{ base.NoOperandsInstruction }
type ICONST_5 struct{ base.NoOperandsInstruction }
type LCONST_0 struct{ base.NoOperandsInstruction }
type LCONST_1 struct{ base.NoOperandsInstruction }

以3条指令为例进行说明。aconst_null指令把null引用推入操作 数栈顶,代码如下

func (self *ACONST_NULL) Execute(frame *rtda.Frame) {
	frame.OperandStack().PushRef(nil)
}

dconst_0指令把double型0推入操作数栈顶,代码如下

func (self *DCONST_0) Execute(frame *rtda.Frame) {
	frame.OperandStack().PushDouble(0.0)
}

iconst_m1指令把int型-1推入操作数栈顶,代码如下:

func (self *ICONST_M1) Execute(frame *rtda.Frame) {
	frame.OperandStack().PushInt(-1)
}

bipush和sipush指令

  • bipush指令
    从操作数中获取一个byte型整数,扩展成int型,然后推入栈顶。
  • sipush指令
    从操作数中获取一个short型整数,扩展成int型,然后推入栈顶。

constants目录下创建 ipush.go文件,在其中定义bipush和sipush指令,代码如下:

type BIPUSH struct { val int8 } // Push byte
type SIPUSH struct { val int16 } // Push short

BIPUSH结构体实现方法如下:

type BIPUSH struct {
    val int8
}

func (self *BIPUSH) FetchOperands(reader *base.BytecodeReader) {
    self.val = reader.ReadInt8()
}
func (self *BIPUSH) Execute(frame *rtda.Frame) {
    i := int32(self.val)
    frame.OperandStack().PushInt(i)
}

五、加载指令

加载指令用于从局部变量表获取变量,并将其推入操作数栈顶。总共有 33 条加载指令,它们按照所操作的变量类型可以分为 6 类:

  1. aload
    系列指令:用于操作
    引用类型
    变量。
  2. dload
    系列指令:用于操作
    double
    类型变量。
  3. fload
    系列指令:用于操作
    float
    变量。
  4. iload
    系列指令:用于操作
    int
    变量。
  5. lload
    系列指令:用于操作
    long
    变量。
  6. xaload
    指令:用于操作数组。

本节将实现其中的 25 条加载指令。数组和xaload系列指令先不实现。

loads目录
下创建
iload.go
文件,在其中定义5 条指令,代码如下:
完整代码移步:
jvmgo

// 从局部变量表加载int类型
type ILOAD struct{ base.Index8Instruction }
type ILOAD_0 struct{ base.NoOperandsInstruction }
type ILOAD_1 struct{ base.NoOperandsInstruction }
type ILOAD_2 struct{ base.NoOperandsInstruction }
type ILOAD_3 struct{ base.NoOperandsInstruction }

为了避免重复代码,定义一个函数供iload系列指令使用,代码如下:

func _iload(frame *rtda.Frame, index uint) {
    val := frame.LocalVars().GetInt(index)
    frame.OperandStack().PushInt(val)
}

iload指令的索引来自操作数,其Execute()方法如下:

func (self *ILOAD) Execute(frame *rtda.Frame) {
	_iload(frame, uint(self.Index))
}

其余4条指令的索引隐含在操作码中,以iload_1为例,其 Execute()方法如下:

func (self *ILOAD_1) Execute(frame *rtda.Frame) {
	_iload(frame, 1)
}

六、存储指令

和加载指令刚好相反,存储指令把变量从操作数栈顶弹出,然后存入局部变量表。

和加载指令一样,存储指令也可以分为6类。以
lstore系列
指令为例进行介绍。
完整代码移步:
jvmgo

instructions\stores目录下创建
lstore.go
文件,在其中定义5条指令,代码如下:

type LSTORE struct{ base.Index8Instruction }
type LSTORE_0 struct{ base.NoOperandsInstruction }
type LSTORE_1 struct{ base.NoOperandsInstruction }
type LSTORE_2 struct{ base.NoOperandsInstruction }
type LSTORE_3 struct{ base.NoOperandsInstruction }

同样定义一个函数供5条指令使用,代码如下:

func _lstore(frame *rtda.Frame, index uint) {
    val := frame.OperandStack().PopLong()
    frame.LocalVars().SetLong(index, val)
}

lstore指令的索引来自操作数,其Execute()方法如下:

func (self *LSTORE) Execute(frame *rtda.Frame) {
	_lstore(frame, uint(self.Index))
}

其余4条指令的索引隐含在操作码中,以lstore_2为例,其 Execute()方法如下

func (self *LSTORE_2) Execute(frame *rtda.Frame) {
	_lstore(frame, 2)
}

七、栈指令

栈指令直接对操作数栈进行操作,共9条:

pop和pop2指令将栈顶变量弹出

dup系列指令复制栈顶变量

swap指令交换栈顶的两个变量

和其他类型的指令不同,栈指令并不关心变量类型。为了实现栈指令,需要给
OperandStack结构体
添加两个方法。
操作数栈实现
rtda\operand_stack.go文件中,在其中定义
PushSlot()

PopSlot()
方法,代码如下:

func (self *OperandStack) PushSlot(slot Slot) {
    self.slots[self.size] = slot
    self.size++
}
func (self *OperandStack) PopSlot() Slot {
    self.size--
    return self.slots[self.size]
}

pop和pop2指令

stack目录下创建pop.go文件,在其中定义 pop和pop2指令,代码如下:

type POP struct{ base.NoOperandsInstruction }
type POP2 struct{ base.NoOperandsInstruction }

pop指令把栈顶变量弹出,代码如下:

func (self *POP) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    stack.PopSlot()
}

pop指令只能用于弹出int、float等占用一个操作数栈位置的变量。

double和long变量在操作数栈中占据两个位置,需要使用pop2指令弹出,代码如下:

func (self *POP2) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    stack.PopSlot()
    stack.PopSlot()
}

dup指令

创建
dup.go
文件,在其中定义6 条指令,代码如下:
完整代码移步:
jvmgo

type DUP struct{ base.NoOperandsInstruction }
type DUP_X1 struct{ base.NoOperandsInstruction }
type DUP_X2 struct{ base.NoOperandsInstruction }
type DUP2 struct{ base.NoOperandsInstruction }
type DUP2_X1 struct{ base.NoOperandsInstruction }
type DUP2_X2 struct{ base.NoOperandsInstruction }

dup指令复制栈顶的单个变量,代码如下:

func (self *DUP) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    slot := stack.PopSlot()
    stack.PushSlot(slot)
    stack.PushSlot(slot)
}

DUP_X1
:复制栈顶操作数一份放在第二个操作数的下方。Execute代码如下:

/*
bottom -> top
[...][c][b][a]
          __/
         |
         V
[...][c][a][b][a]
*/
func (self *DUP_X1) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    slot1 := stack.PopSlot()
    slot2 := stack.PopSlot()
    stack.PushSlot(slot1)
    stack.PushSlot(slot2)
    stack.PushSlot(slot1)
}

DUP_X2
:复制栈顶操作数栈的一个或两个值,并将它们插入到操作数栈中的第三个值的下面。

/*
bottom -> top
[...][c][b][a]
       _____/
      |
      V
[...][a][c][b][a]
*/
func (self *DUP_X2) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    slot1 := stack.PopSlot()
    slot2 := stack.PopSlot()
    slot3 := stack.PopSlot()
    stack.PushSlot(slot1)
    stack.PushSlot(slot3)
    stack.PushSlot(slot2)
    stack.PushSlot(slot1)
}

swap指令

swap指令作用是交换栈顶的两个操作数

下创建
swap.go
文件,在其中定义
swap指令
,代码如下:

type SWAP struct{ base.NoOperandsInstruction }

Execute()方法如下

func (self *SWAP) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
slot1 := stack.PopSlot()
slot2 := stack.PopSlot()
stack.PushSlot(slot1)
stack.PushSlot(slot2)
}

八、数学指令

数学指令大致对应Java语言中的加、减、乘、除等数学运算符。

数学指令包括算术指令、位移指令和布尔运算指令等,共37条,将全部在本节实现。

算术指令

算术指令又可以进一步分为:

  • 加法(add)指令
  • 减法(sub)指令
  • 乘法(mul)指令
  • 除法(div)指令
  • 求余(rem)指令
  • 取反(neg)指令

加、减、乘、除和取反指令都比较简单,本节以复杂的
求余指令
介绍。

math目录
下创建
rem.go
文件,在其中定义4条求余指令,代码如下:

type DREM struct{ base.NoOperandsInstruction }
type FREM struct{ base.NoOperandsInstruction }
type IREM struct{ base.NoOperandsInstruction }
type LREM struct{ base.NoOperandsInstruction }
  • DREM
    结构体:表示对双精度浮点数 (
    double
    ) 执行取余操作。
  • FREM
    结构体:表示对单精度浮点数 (
    float
    ) 执行取余操作
  • IREM
    结构体:表示对整数 (
    int
    ) 执行取余操作。
  • LREM
    结构体:表示对长整数 (
    long
    ) 执行取余操作。

irem

lrem
代码差不多,以
irem
为例,其
Execute()
方法如下:

func (self *IREM) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    v2 := stack.PopInt()
    v1 := stack.PopInt()
    if v2 == 0 {
    	panic("java.lang.ArithmeticException: / by zero")
    }
    result := v1 % v2
    stack.PushInt(result)
}

先从操作数栈中弹出两个int变量,求余,然后把结果推入操作 数栈。

注意!对int或long变量做除法和求余运算时,是有可能抛出ArithmeticException异常的。

frem和drem指令差不多,以 drem为例,其Execute()方法如下:

func (self *DREM) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    v2 := stack.PopDouble()
    v1 := stack.PopDouble()
    result := math.Mod(v1, v2)
    stack.PushDouble(result)
}

Go语言没有给浮点数类型定义求余操作符,所以需要使用
math包

Mod()
函数。

浮点数类型因为有Infinity(无穷大)值,所以即使是除零,也不会导致ArithmeticException异常抛出

位移指令

分为左移和右移

  • 左移
  • 右移
    • 算术右移(有符号右移)
    • 逻辑右移(无符号右移)两种。

算术右移和逻 辑位移的区别仅在于符号位的扩展,如下面的Java代码所示。

int x = -1;
println(Integer.toBinaryString(x)); // 66666666666666666666666666666666666666666666666666666666666611
println(Integer.toBinaryString(x >> 8)); // 66666666666666666666666666666666666666666666666666666666666611
println(Integer.toBinaryString(x >>> 8)); // 00000000666666666666666666666666666666666666666666666666

math目录下创建sh.go文件,在其中定义6条 位移指令,代码如下

type ISHL struct{ base.NoOperandsInstruction } // int左位移
type ISHR struct{ base.NoOperandsInstruction } // int算术右位移
type IUSHR struct{ base.NoOperandsInstruction } // int逻辑右位移(无符号右移位)
type LSHL struct{ base.NoOperandsInstruction } // long左位移
type LSHR struct{ base.NoOperandsInstruction } // long算术右位移
type LUSHR struct{ base.NoOperandsInstruction } // long逻辑右移位(无符号右移位)

左移

左移指令比较简单,以
ishl
指令为例,其
Execute()
方法如下:

func (self *ISHL) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    v2 := stack.PopInt()
    v1 := stack.PopInt()
    s := uint32(v2) & 0x1f
    result := v1 << s
    stack.PushInt(result)
}

先从操作数栈中弹出两个int变量v2和v1。v1是要进行位移操作的变量,v2指出要移位多少比特。位移之后,把结果推入操作数栈。

s := uint32(v2) & 0x1f
:这行代码将被左移的位数
v2
强制转换为
uint32
类型,然后执行按位与操作(
&
)与常数
0x1f

这是为了确保左移的位数在范围 0 到 31 内,因为在 Java 中,左移操作最多只能左移 31 位,超出这个范围的位数将被忽略。

这里注意两点:

int变量只有32位,所以只取v2的前5个比特就 足够表示位移位数了

Go语言位移操作符右侧必须是无符号 整数,所以需要对v2进行类型转换

右移

算数右移

算术右移指令需要扩展符号位,代码和左移指令基本上差不多。以
lshr
指令为例,其
Execute()
方法如下:

func (self *LSHR) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    v2 := stack.PopInt()
    //long变量有64位,所以取v2的前6个比特。
    v1 := stack.PopLong()
    s := uint32(v2) & 0x3f
    result := v1 >> s
    stack.PushLong(result)
}

s := uint32(v2) & 0x1f:

提取
v2
变量的最低的 6 位,将其他位设置为 0,并将结果存储在
s
变量中。这是为了限制右移的位数在 0 到 63 之间,因为在 Java 中,long类型右移操作最多只能右移 63 位

逻辑右移

无符号右移位,以
iushr
为例,在移位前,先将v2转化为正数,再进行移位,最后转化为int32类型,如下代码所示:

func (self *IUSHR) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    v2 := stack.PopInt()
    v1 := stack.PopInt()
    s := uint32(v2) & 0x1f
    result := int32(uint32(v1) >> s)
    stack.PushInt(result)
}

布尔运算指令

布尔运算指令只能操作int和long变量,分为:

  • 按位与(and)
  • 按位 或(or)
  • 按位异或(xor)

math
目录下创建
and.go
文件,在其中定义
iand

land
指令,代码如下:

type IAND struct{ base.NoOperandsInstruction }
type LAND struct{ base.NoOperandsInstruction }

以iand指令为例,其Execute()方法如下:

func (self *IAND) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    v2 := stack.PopInt()
    v1 := stack.PopInt()
    result := v1 & v2
    stack.PushInt(result)
}

iinc指令

iinc指令
给局部变量表中的int变量增加常量值,局部变量表索引和常量值都由指令的操作数提供。

math目录下创建
iinc.go
文件,在其中定义
iinc指令
,代码如下:

type IINC struct {
    //索引
	Index uint
    //常量值
	Const int32
}
  • index
    :一个字节,表示局部变量表中要增加值的变量的索引。这个索引指定了要修改的局部变量。
  • const
    :一个有符号字节,表示要增加的常数值。这个常数值将与局部变量的当前值相加,并将结果存储回同一个局部变量。

FetchOperands()
函数从
字节码
里读取操作数,代码如下:

func (self *IINC) FetchOperands(reader *base.BytecodeReader) {
    self.Index = uint(reader.ReadUint8())
    self.Const = int32(reader.ReadInt8())
}

Execute()方法从局部变量表中读取变量,给它加上常量值,再把结果写回
局部变量表
,代码如下

func (self *IINC) Execute(frame *rtda.Frame) {
    localVars := frame.LocalVars()
    val := localVars.GetInt(self.Index)
    val += self.Const
    localVars.SetInt(self.Index, val)
}

九、类型转换指令

类型转换指令大致对应Java语言中的基本类型强制转换操作。 类型转换指令有共15条,将全部在本节实现。

引用类型转换对应的是
checkcast指令
,将在后续完成。

类型转换指令根据被
转换变量的类型
分为四种系列:

  • i2x 系列指令
    :这些指令将整数(int)变量强制转换为其他类型。
  • l2x 系列指令
    :这些指令将长整数(long)变量强制转换为其他类型。
  • f2x 系列指令
    :这些指令将浮点数(float)变量强制转换为其他类型。
  • d2x 系列指令
    :这些指令将双精度浮点数(double)变量强制转换为其他类型。

这些类型转换指令允许将不同类型的数据进行强制类型转换,以满足特定的计算或操作需求。


d2x系列
指令为例进行讨论。

conversions目录
下创建
d2x.go
文件,在其中 定义d2f、d2i和d2l指令,代码如下

type D2F struct{ base.NoOperandsInstruction }
type D2I struct{ base.NoOperandsInstruction }
type D2L struct{ base.NoOperandsInstruction }


d2i指令
为例,它的
Execute()
方法如下:

func (self *D2I) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    d := stack.PopDouble()
    i := int32(d)
    stack.PushInt(i)
}

因为Go语言可以很方便地转换各种基本类型的变量,所以类型转换指令实现起来还是比较容易的。

十、比较指令

比较指令可以分为两类:

  • 将比较结果推入操作数栈顶
  • 根据比较结果跳转

比较指令是编译器实现if-else、for、while等语句的基石,共有19条

lcmp指令

lcmp指令
用于比较long变量。

comparisons目录下创建
lcmp.go
文件,在其中定义
lcmp指令
,代码如下:

type LCMP struct{ base.NoOperandsInstruction }

Execute()
方法把栈顶的两个long变量弹出,进行比较,然后把比较结果(int型0、1或-1)推入栈顶,代码如下:

func (self *LCMP) Execute(frame *rtda.Frame) {
	stack := frame.OperandStack()
	v2 := stack.PopLong()
	v1 := stack.PopLong()
	if v1 > v2 {
		stack.PushInt(1)
	} else if v1 == v2 {
		stack.PushInt(0)
	} else {
		stack.PushInt(-1)
	}
}

fcmp和dcmp指令

fcmpg

fcmpl指令
用于比较float变量,它们的区别是对于非数字参与,
fcmpg
会默认为其大于任何非NaN值,
fcmpl
则相反。

comparisons目录下创建fcmp.go文件,在其中定义
fcmpg

fcmpl指令
,代码如下:

type FCMPG struct{ base.NoOperandsInstruction }
type FCMPL struct{ base.NoOperandsInstruction }

由于浮点数计算有可能产生NaN(Not a Number)值,所以比较两个浮点数时,除了大于、等于、小于之外,
还有第4种结果:无法比较。

编写一个函数来统一比较float变量,如下:

func _fcmp(frame *rtda.Frame, gFlag bool) {
	stack := frame.OperandStack()
	v2 := stack.PopFloat()
	v1 := stack.PopFloat()
	if v1 > v2 {
		stack.PushInt(1)
	} else if v1 == v2 {
		stack.PushInt(0)
	} else if v1 < v2 {
		stack.PushInt(-1)
	} else if gFlag {
		stack.PushInt(1)
	} else {
		stack.PushInt(-1)
	}
}

Java虚拟机规范:浮点数比较指令
fcmpl

fcmpg
的规范要求首先弹出
v2
,然后是
v1
,以便进行浮点数比较。

Execute()如下:

func (self *FCMPG) Execute(frame *rtda.Frame) {
    _fcmp(frame, true)
}
func (self *FCMPL) Execute(frame *rtda.Frame) {
    _fcmp(frame, false)
}

if<cond>指令

if<cond>
指令是 Java 字节码中的条件分支指令,它根据条件
<cond>
来执行不同的分支。
条件
<cond>
可以是各种比较操作,比如等于、不等于、大于、小于等等。

常见的
if<cond>
指令包括:

  • ifeq
    : 如果栈顶的值等于0,则跳转。
  • ifne
    : 如果栈顶的值不等于0,则跳转。
  • iflt
    : 如果栈顶的值小于0,则跳转。
  • ifge
    : 如果栈顶的值大于或等于0,则跳转。
  • ifgt
    : 如果栈顶的值大于0,则跳转。
  • ifle
    : 如果栈顶的值小于或等于0,则跳转。

创建ifcond.go文件,在其中定义6条if指令,代码如下:

type IFEQ struct{ base.BranchInstruction }
type IFNE struct{ base.BranchInstruction }
type IFLT struct{ base.BranchInstruction }
type IFLE struct{ base.BranchInstruction }
type IFGT struct{ base.BranchInstruction }
type IFGE struct{ base.BranchInstruction }


ifeq指令
为例,其Execute()方法如下:

func (self *IFEQ) Execute(frame *rtda.Frame) {
    val := frame.OperandStack().PopInt()
    if val == 0 {
    	base.Branch(frame, self.Offset)
	}
}

真正的跳转逻辑在Branch()函数中。因为这个函数在很多指令中都会用到,所以定义在base\branch_logic.go 文件中,代码如下:

func Branch(frame *rtda.Frame, offset int) {
	pc := frame.Thread().PC()
	nextPC := pc + offset
	frame.SetNextPC(nextPC)
}

if_icmp<cond>指令

if_icmp<cond>
指令是 Java 字节码中的一类条件分支指令,它用于对比两个整数值,根据比较的结果来执行条件分支。这些指令的操作数栈上通常有两个整数值,它们分别用于比较。

这类指令包括:

  • if_icmpeq
    : 如果两个整数相等,则跳转。
  • if_icmpne
    : 如果两个整数不相等,则跳转。
  • if_icmplt
    : 如果第一个整数小于第二个整数,则跳转。
  • if_icmpge
    : 如果第一个整数大于等于第二个整数,则跳转。
  • if_icmpgt
    : 如果第一个整数大于第二个整数,则跳转。
  • if_icmple
    : 如果第一个整数小于等于第二个整数,则跳转。

创建if_icmp.go文件,在 其中定义6条if_icmp指令,代码如下:

type IF_ICMPEQ struct{ base.BranchInstruction }
type IF_ICMPNE struct{ base.BranchInstruction }
type IF_ICMPLT struct{ base.BranchInstruction }
type IF_ICMPLE struct{ base.BranchInstruction }
type IF_ICMPGT struct{ base.BranchInstruction }
type IF_ICMPGE struct{ base.BranchInstruction }

以if_icmpne指令 为例,其Execute()方法如下:

func (self *IF_ICMPNE) Execute(frame *rtda.Frame) {
    if val1, val2 := _icmpPop(frame); val1 != val2 {
       base.Branch(frame, self.Offset)
    }
}
func _icmpPop(frame *rtda.Frame) (val1, val2 int32) {
	stack := frame.OperandStack()
	val2 = stack.PopInt()
	val1 = stack.PopInt()
	return
}

if_acmp<cond>指令

if_acmp<cond>
指令是 Java 字节码中的一类条件分支指令,用于比较两个引用类型的对象引用,根据比较的结果来执行条件分支。这些指令的操作数栈上通常有两个对象引用,它们分别用于比较。

这类指令包括:

  • if_acmpeq
    : 如果两个引用相等,则跳转。
  • if_acmpne
    : 如果两个引用不相等,则跳转。

创建if_acmp.go文件,在 其中定义两条if_acmp指令,代码如下:

type IF_ACMPEQ struct{ base.BranchInstruction }
type IF_ACMPNE struct{ base.BranchInstruction }

以if_acmpeq指令为例,其Execute()方法如下:

func (self *IF_ACMPEQ) Execute(frame *rtda.Frame) {
    stack := frame.OperandStack()
    ref2 := stack.PopRef()
    ref1 := stack.PopRef()
    if ref1 == ref2 {
    	base.Branch(frame, self.Offset)
    }
}

十一、控制指令

  • 控制指令共有 11 条。
  • 在 Java 6 之前,
    jsr ret 指令用于实现 finally 子句
    。从 Java 6 开始,Oracle 的 Java 编译器不再使用这两条指令。
  • return
    系列指令有 6 条,用于从方法调用中返回,将在后续实现。
  • 本节将实现剩下的 3 条指令:
    goto

    tableswitch

    lookupswitch

这些指令用于控制程序执行流,包括条件分支和无条件跳转等操作。其中,
goto
用于无条件跳转到指定的目标位置,而
tableswitch

lookupswitch
用于根据条件跳转到不同的目标位置。

control目录下创建goto.go文件,在其中定义 goto指令,代码如下:

wmproxy

wmproxy
已用
Rust
实现
http/https
代理,
socks5
代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现
websocket
代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子

项目地址

国内: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

简单介绍websocket

WebSocket
是一种在单个 TCP 连接上进行全双工通信的协议,它使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。WebSocket 通信协议于 2011 年被 IETF 定为标准
RFC 6455
,并由
RFC7936
补充规范。WebSocket API 也被 W3C 定为标准。

也就是在web环境中,websocket就是socket的一种标准形式的体现。类似的还要SSE基于HTTP中的
text/event-stream

源码文件含义

协议层的编码解码主要在
webparse/ws

  • frame_header
    协议头的解码与编码
  • dataframe
    基础单位为帧,存在多帧组成一个数据包的情况
  • message
    协议的基本信息,包含
    Text
    必须为UTF-8字符串文本,
    Binary
    二进制数据流,
    Close
    关闭信息,
    Ping

    Pong
    用来做心跳包相关的信息。
  • mask
    是否为数据进行基本的加密,服务端要求客户端传来的数据必须加密

网络处理层的源码主要在
wenmeng/ws

  • codec/framed_read
    每一帧的读,以帧为单位进行读取
  • codec/framed_write
    每一帧的写,以帧为单位进行写入
  • state/state_handshake
    websocket连接内部的握手状态
  • client_connection
    客户端的状态连接
  • server_connection
    服务端的状态连接
  • control
    状态的控制,写入读取的pending等,核心处理源码
  • handshake
    定义
    on_open
    回调后的
    WsHandshake
  • option
    定义
    on_open
    回调后返回的
    WsOption
    类,当下只包含定时器,即客户端多久时间唤醒一次
    interval
  • ws_trait
    websocket的核心回调
#[async_trait]
pub trait WsTrait: Send {
    /// 通过请求连接构建出返回的握手连接信息
    #[inline]
    fn on_request(&mut self, req: &RecvRequest) -> ProtResult<RecvResponse> {
        // warn!("Handler received request:\n{}", req);
        WsHandshake::build_request(req)
    }

    /// 握手完成后之后的回调,服务端返回了Response之后就认为握手成功
    fn on_open(&mut self, shake: WsHandshake) -> ProtResult<Option<WsOption>>;

    /// 接受到远端的关闭消息
    async fn on_close(&mut self, reason: &Option<CloseData>) {}

    /// 服务内部出现了错误代码
    async fn on_error(&mut self, err: ProtError) {}

    /// 收到来在远端的ping消息, 默认返回pong消息
    async fn on_ping(&mut self, val: Vec<u8>) -> ProtResult<OwnedMessage> {
        return Ok(OwnedMessage::Pong(val));
    }

    /// 收到来在远端的pong消息, 默认不做任何处理, 可自定义处理如ttl等
    async fn on_pong(&mut self, val: Vec<u8>) {}

    /// 收到来在远端的message消息, 必须覆写该函数
    async fn on_message(&mut self, msg: OwnedMessage) -> ProtResult<()>;

    /// 定时器定时按间隔时间返回
    async fn on_interval(&mut self, option: &mut Option<WsOption>) -> ProtResult<()> {
        Ok(())
    }
    
    /// 将当前trait转化成Any,仅需当需要重新获取回调处理的时候进行处理
    fn as_any(&self) -> Option<&dyn Any> {
        None
    }

    /// 将当前trait转化成mut Any,仅需当需要重新获取回调处理的时候进行处理
    fn as_any_mut(&mut self) -> Option<&mut dyn Any> {
        None
    }
}

服务端基础demo

建立一个本地监听8081的ws端口,完整源码
ws_server

建立监听类:


struct Operate {
    sender: Option<Sender<OwnedMessage>>,
}

#[async_trait]
impl WsTrait for Operate {
    fn on_open(&mut self, shake: WsHandshake) -> ProtResult<Option<WsOption>> {
        self.sender = Some(shake.sender);
        Ok(Some(WsOption::new(Duration::from_secs(10))))
    }

    async fn on_message(&mut self, msg: OwnedMessage) -> ProtResult<()> {
        println!("callback on message = {:?}", msg);
        let _ = self
            .sender
            .as_mut()
            .unwrap()
            .send(OwnedMessage::Text("from server".to_string()))
            .await;
        let _ = self.sender.as_mut().unwrap().send(msg).await;
        Ok(())
    }

    async fn on_interval(&mut self, _option: &mut Option<WsOption>) -> ProtResult<()> {
        println!("on_interval!!!!!!!");
        Ok(())
    }
}

然后启动服务器监听:

async fn run_main() -> Result<(), Box<dyn Error>> {
    let addr = "127.0.0.1:8081".to_string();
    let server = TcpListener::bind(&addr).await?;
    println!("Listening on: {}", addr);
    loop {
        let (stream, addr) = server.accept().await?;
        tokio::spawn(async move {
            let mut server = Server::new(stream, Some(addr));
            let operate = Operate { sender: None };
            // 设置服务回调
            server.set_callback_ws(Box::new(operate));
            let e = server.incoming().await;
            println!("close server ==== addr = {:?} e = {:?}", addr, e);
        });
    }
}

此时即可实现websocket的监听及处理。

客户端demo

当下客户端demo需要能接受终端的输入,并向服务器发送数据,所以需要自己构建sender

建立客户端连接,在这里我们手动构建了一个sender/receiver对。

async fn run_main() -> ProtResult<()> {
    // 自己手动构建数据对,并将receiver传给服务端
    let (sender, receiver) = channel(10);
    let sender_clone = sender.clone();
    tokio::spawn(async move {
        let url = "ws://127.0.0.1:8081";
        let mut client = Client::builder()
            .url(url)
            .unwrap()
            .connect()
            .await
            .unwrap();
        client.set_callback_ws(Box::new(Operate { sender:Some(sender_clone), receiver: Some(receiver) }));
        client.wait_ws_operate().await.unwrap();
    });
    loop {
        let mut buffer = String::new();
        let stdin = io::stdin(); // We get `Stdin` here.
        stdin.read_line(&mut buffer)?;
        sender.send(OwnedMessage::Text(buffer)).await?;
    }
    Ok(())
}

监听实现

struct Operate {
    sender: Option<Sender<OwnedMessage>>,
    receiver: Option<Receiver<OwnedMessage>>,
}

#[async_trait]
impl WsTrait for Operate {
    fn on_open(&mut self, shake: WsHandshake) -> ProtResult<Option<WsOption>> {
        // 将receiver传给控制中心, 以让其用该receiver做接收
        let mut option = WsOption::new(Duration::from_secs(1000));
        if self.receiver.is_some() {
            option.set_receiver(self.receiver.take().unwrap());
        }
        if self.sender.is_none() {
            self.sender = Some(shake.sender);
        }
        Ok(Some(option))
    }

    async fn on_message(&mut self, msg: OwnedMessage) -> ProtResult<()> {
        println!("callback on message = {:?}", msg);
        let _ = self
            .sender
            .as_mut()
            .unwrap()
            .send(OwnedMessage::Text("from client".to_string()))
            .await;
        let _ = self.sender.as_mut().unwrap().send(msg).await;
        Ok(())
    }

    async fn on_interval(&mut self, _option: &mut Option<WsOption>) -> ProtResult<()> {
        println!("on_interval!!!!!!!");
        Ok(())
    }
}

接口说明

Client

Server
为了同时兼容HTTP服务,即握手用的为HTTP的前半段请求,选择了回调用
Box<dyn Trait>
的形式来做回调函数的处理。

pub struct Server<T>
where
    T: AsyncRead + AsyncWrite + Unpin + Sized,
{
    /// http的接口回调, 处理http服务器
    callback_http: Option<Box<dyn HttpTrait>>,
    /// websocket的接口回调, 处理websocket服务器
    callback_ws: Option<Box<dyn WsTrait>>,
    // ...
}

他们两个可能是同时存在,或者单个存在的,即当作服务的时候,可能仅对
/ws
进行websocket的升级,其它的仅仅是http服务,所以需要能单独又能聚合的处理数据。而单存的websocket仅需
WsTrait
回调。

即在
pub async fn incoming(&mut self) -> ProtResult<()>
处理服务的时候不在传入回调地址,改成预先设置。达到灵活处理的目的。且接口比较清晰。

小结

wenmeng库当前已支持HTTP1.1/HTTP2/WEBSOCKET,在浏览器的环境中websocket是必不可缺少的存在,当然有很多原生的服务中用的都是socket,下一章中,我们将实现websocket与tcp的互转,以便一些tcp的程序可以服务web的服务。

点击
[关注]

[在看]

[点赞]
是对作者最大的支持

1、文档说明

本方法是将新增字段,展示在MIGO的新增页签中,并保存到自建表。

新增页签的方法,和采购订单新增页签的方法原理基本一致,都是需要创建函数组,并实现相应方法和屏幕,并在增强中调用该函数组,展示出屏幕,实现对应操作。

2、实现过程

2.1、创建自建表和结构

自建表中存储物料凭证主键和增强的字段ZNUM单据号

参考自建表,创建对应的结构,用于函数组数据传递

创建表类型,用于增强实施类

2.2、增强和函数组

为了清楚的展示增强和调用的函数之间的关系,此处将增强和函数实现放在一起讲解

2.2.1、创建增强实施和函数组

根据增强点MB_MIGO_BADI创建实施

点击实施类,添加表类型到实施类属性

创建函数组ZEMIGO,并声明全局变量

2.2.2、初始化

实现增强方法IF_EX_MB_MIGO_BADI~INIT

"--------------------@斌将军--------------------
method IF_EX_MB_MIGO_BADI~INIT.APPEND gf_class_id TOct_init.
endmethod.
"--------------------@斌将军--------------------

2.2.3、实现PBO

创建SET函数ZEMIGO_SET_DATA

创建子屏幕9001,类型选择子屏幕

实现增强方法IF_EX_MB_MIGO_BADI~PBO_DETAIL

"--------------------@斌将军--------------------
METHODif_ex_mb_migo_badi~pbo_detail.IF gf_class_id =i_class_id.DATA: wa_item TYPEzspp001.CHECK i_line_id IS NOT INITIAL.
e_cprog
= 'SAPLZEMIGO'."函数组
e_dynnr
= '9001'."展示的页签屏幕
e_heading
= '客户数据'."页签标题

g_line_id
=i_line_id.READ TABLE it_item INTO wa_item WITH KEY line_id =i_line_id.CALL FUNCTION 'ZEMIGO_SET_DATA' EXPORTINGi_input=wa_item.ENDIF.ENDMETHOD."--------------------@斌将军--------------------

2.2.4、实现PAI

创建GET函数ZEMIGO_GET_DATA

实现增强方法IF_EX_MB_MIGO_BADI~PAI_DETAIL

"--------------------@斌将军--------------------
METHODif_ex_mb_migo_badi~pai_detail.DATA: wa_item_new TYPEzspp001,
wa_item_old
TYPEzspp001.CHECK i_line_id <> 0.CALL FUNCTION 'ZEMIGO_GET_DATA' IMPORTINGe_out=wa_item_new.MODIFY it_item FROM wa_item_new TRANSPORTING znum WHERE line_id =i_line_id.READ TABLE it_item INTO wa_item_old WITH KEY line_id =i_line_id.IF wa_item_new-line_id <>i_line_id.
e_force_change
= 'X'.ENDIF.ENDMETHOD."--------------------@斌将军--------------------

2.2.5、实现MODIFY

实现增强方法IF_EX_MB_MIGO_BADI~LINE_MODIFY

"--------------------@斌将军--------------------
METHODif_ex_mb_migo_badi~line_modify.DATA: wa_item_new      TYPEzspp001,
wa_item_old
TYPEzspp001,
wa_ztmm_i_migo01
TYPEztpp001,
l_subrc
TYPE sy-subrc.READ TABLE it_item INTO wa_item_old WITH KEY line_id =i_line_id.
l_subrc
= sy-subrc.IF sy-subrc <> 0.IF cs_goitem-mblnr IS NOT INITIAL AND cs_goitem-mjahr IS NOT INITIAL AND cs_goitem-zeile IS NOT INITIAL.SELECT SINGLE * FROMztpp001INTOwa_ztmm_i_migo01WHERE mblnr = cs_goitem-mblnrAND mjahr = cs_goitem-mjahrAND zeile = cs_goitem-zeile.IF wa_ztmm_i_migo01 IS NOT INITIAL.MOVE-CORRESPONDING wa_ztmm_i_migo01 TOwa_item_new.ENDIF.ENDIF.

wa_item_new
-line_id =i_line_id.INSERT wa_item_new INTO TABLEit_item.ELSE.CHECK g_line_id =i_line_id.CALL FUNCTION 'ZEMIGO_GET_DATA' IMPORTINGe_out=wa_item_new.*wa_item_new-line_id = i_line_id. MODIFY it_item FROM wa_item_new TRANSPORTING znum WHERE line_id =i_line_id.ENDIF.ENDMETHOD."--------------------@斌将军--------------------

2.2.6、实现DELETE

实现增强方法IF_EX_MB_MIGO_BADI~LINE_DELETE

"--------------------@斌将军--------------------
METHODIF_EX_MB_MIGO_BADI~LINE_DELETE.DELETE TABLE it_item WITH TABLE KEY line_id =i_line_id.ENDMETHOD."--------------------@斌将军--------------------

2.2.7、实现RESET

实现增强方法IF_EX_MB_MIGO_BADI~RESET

"--------------------@斌将军--------------------
METHODIF_EX_MB_MIGO_BADI~RESET.*Clear all internal data:
  CLEAR: gt_extdata,
g_no_input,
gs_exdata_header,
g_cancel,
it_item,
g_line_id.
ENDMETHOD."--------------------@斌将军--------------------

2.2.8、实现存表

创建UPDATE函数ZEMIGO_UPDATE_DATA

选择处理类型

实现增强方法IF_EX_MB_MIGO_BADI~POST_DOCUMENT

"--------------------@斌将军--------------------
METHODif_ex_mb_migo_badi~post_document.DATA: wa_ztmm_i_migo01 TYPEztpp001,
lt_ztmm_i_migo01
TYPE TABLE OFztpp001,
wa_item
TYPEzspp001,
wa_mseg
TYPEmseg.IF it_item IS NOT INITIAL.LOOP AT it_item INTOwa_item.IF g_cancel IS INITIAL.READ TABLE it_mseg INTOwa_msegWITH KEY line_id = wa_item-line_id.ELSE.READ TABLE it_mseg INTOwa_msegWITH KEY smbln = wa_item-mblnr
smblp
= wa_item-zeile
sjahr
= wa_item-mjahr.ENDIF.IF sy-subrc IS INITIAL.MOVE-CORRESPONDING wa_item TOwa_ztmm_i_migo01.MOVE-CORRESPONDING wa_mseg TOwa_ztmm_i_migo01.APPEND wa_ztmm_i_migo01 TOlt_ztmm_i_migo01.ENDIF.ENDLOOP.CALL FUNCTION 'ZEMIGO_UPDATE_DATA' IN UPDATE TASK TABLESt_item=lt_ztmm_i_migo01.ENDIF.ENDMETHOD."--------------------@斌将军--------------------

2.2.9、控制编辑状态

可以通过里面对应的ACTION,来控制增强字段的显示效果,比如在A04显示物料凭证时,设置增强字段为不可更改

创建函数ZEMIGO_SET_GOACTION接收ACTION

实现屏幕流

实现增强方法IF_EX_MB_MIGO_BADI~MODE_SET

"--------------------@斌将军--------------------
METHODif_ex_mb_migo_badi~mode_set.*ACTION and REFDOC will discribe the mode of transaction MIGO.*----------------------------------------------------------------------*i_action:*A01 = Goods receipt*A02 = Return delivery*A03 = Cancellation*A04 = Display*A05 = Release GR bl.st.*A06 = Subsequent deliv.*A07 = Goods issue*
*
i_refdoc:*R01 = Purchase order*R02 = Material document*R03 = Delivery note*R04 = Inbound delivery*R05 = Outbound delivery*R06 = Transport*R07 = Transport ID code*R08 = Order*R09 = Reservation*R10 = Other GR*----------------------------------------------------------------------- *In case of 'DISPLAY' the global field G_NO_INPUT will be set to 'X'.*The result is that a different external subscreen will be choosen in*method PBO_DETAIL. IF i_action = 'A04' OR i_action = 'A03'.
g_no_input
= 'X'.ENDIF.*In case of 'CANCEL' the global field G_CANCEL will be set to 'X'.*The result is that in method POST_DOCUMENT a different handling is*used IF i_action = 'A03'.
g_cancel
= 'X'.ENDIF.CALL FUNCTION 'ZEMIGO_SET_GOACTION' EXPORTINGi_goaction=i_action.ENDMETHOD."--------------------@斌将军--------------------

2.3、测试效果

存表成功

到此,MIGO新增页签就实现了

定期更文,
欢迎关注