编程语言兼容性

本文要讨论的兼容性是指上层应用软件对不同架构芯片及各类板卡硬件的兼容性。本文将聚焦于各类编程语言的兼容性都是怎么引入的,以便于为后续对拓展平台软件生态需要做哪些工作提供一些指导。

什么样的软件具有好的兼容性?

对于一款软件,如果能够提供不同平台的二进制、或者仅提供一个二进制就能够在不同平台上跑,那么说明这款软件的兼容性很完备。但实际上受限于编程语言本身的能力、性能等约束,想达到这种目标往往比较困难。

什么时候会出现兼容性问题?

  • 当代码中硬编码了汇编语言
  • 当代码中调用了so库,而该so库还被打包到最终的可运行包里
  • 当代码中调用了so库,且该so库是由软件源码包中的c/c++等待编译文件编译出来的

以上这些情况均会导致软件与平台相关,进而导致软件必须提供不同平台编译版本才能满足软件在指定平台的可运行。

只要软件和平台相关,那么当指定平台缺少该软件的预编译包时,整个上层应用在该平台上的安装就会失败,从而带来不好的兼容性体验。

那么,如何评价一款软件在给定平台的兼容性情况?

首先需要明确的是,兼容性达成情况是一个bool类型。软件是否兼容与软件有多么不兼容,后者会影响到你去做迁移适配的动作吗?难道软件严重不兼容就不做平台适配了?并不是的,对于给定的平台,我们想让软件跑起来,无论它具有多少不兼容代码,都得一一解决。唯有此,才能达成软件运行的目的。所以,评价软件兼容性是一个非黑即白的结论。

如何识别软件是否需要做兼容性?

  • 分析源码
  • 直接在指定平台上编译运行

针对第一点,以下挑选了top流行语言来列举各种可能存在的硬件兼容性情况。

javascript

代码调用so

javascript调用so,使用node-ffi包:node-ffi/node-ffi: Node.js Foreign Function Interface (github.com)

1
2
3
4
5
6
7
8
9
10
11
12
var ffi = require('ffi');

var libm = ffi.Library('libm', {
'ceil': [ 'double', [ 'double' ] ]
});
libm.ceil(1.5); // 2

// You can also access just functions in the current process by passing a null
var current = ffi.Library(null, {
'atoi': [ 'int', [ 'string' ] ]
});
current.atoi('1234'); // 1234

兼容性识别关键点:检查编译后的包是否包含so。

代码包含c/c++

有些javascript包中带有其他编程语言,比如node-ffi:https://api.github.com/repos/node-ffi/node-ffi/languages

1
2
3
4
5
6
{
"JavaScript": 65311,
"C++": 35850,
"C": 1364,
"Python": 921
}

可以看到,js包里面具有c/c++编译型语言,这种包自然会导致平台兼容性问题。

兼容性识别关键点:检查源码中是否包含编译型语言。

assembly技术

javascript运行会被浏览器翻译为assembly中间机器语言,这里的机器语言并非CPU相关,真正运行还要再转义一次。

这篇文章以一种自定义的JS-ASM来描绘java assembly是怎么工作的:JavaScript Assembler Language Specification (JS-ASM) - CodeProject

npm包

npm包背景知识

npm包制作和发布参考:How to Create and Publish an NPM Package – a Step-by-Step Guide (freecodecamp.org)

制作npm包有两种方式:

1
2
3
4
5
6
7
8
# 方式1:link形式
npm link # 将当前npm工程本地全局发布
npm link <package-name> # 在其他工程目录下通过该命令引用

# 方式2:离线包形式
npm pack # 将当前npm工程打包为一个tgz
cp /path/to/<package>.tgz . # 在其他工程目录下通过该命令集成
# 修改package.json,指定<package-name>: <package>.tgz

npm包安装过程解读:How npm install Works Internally? - DEV Community

npm包工作的核心文件是package.json,该文件内定义了npm如何对该包进行操作,包括install、serve、test等。

如果没有特别说明,包内index.js为npm包的入口(entrypoint),新包发布所能使用的功能函数统一从这里出口(export)

了解这些背景知识后,我们看下有哪些情况npm包在安装时会触发兼容性问题

案例1:Npm install 2.8.2 fails on ARM64, asks to upgrade to 2.5.1 or later - core - Meteor.js forums

该案例里npm包内具有平台检测硬编码,之所以做这类平台限制,是因为该包依赖了mongodb,而mongodb是平台相关,为适配arm,做了如下修复:

Support ARM on Linux by aquinoit · Pull Request #12160 · meteor/meteor (github.com)

image-20230611113916798

案例2:Unable to npm install on M1 · Issue #668 · twilio/twilio-video-app-react (github.com)

该报错是因为npm安装过程中apt install chromium-browser命令执行失败导致,最后通过手动补全chromium依赖解决。

结合以上案例可以得出,npm包在安装过程中体现的兼容性问题,取决于npm包安装脚本是否有外界依赖引入。

兼容性识别关键点:直接跑一遍npm install来检验

python

代码调用c

这篇文章介绍了各种python调用c的方法,并给出了优劣势对比:Python Bindings: Calling C or C++ From Python – Real Python

ctypes内置包会处理所有跟c、so的交互:

1
2
3
4
5
from ctypes import *
# either
libc = cdll.LoadLibrary("libc.so.6")
# or
libc = CDLL("libc.so.6")

兼容性识别关键点:识别源码中是否带有so名称,编译好的包是否具有so。

wheel包

wheel包背景知识

和npm包一样,wheel包也有一个类似package.json的核心文件来承载包的元数据信息,该核心文件有一个演进史:

What’s the difference between setup.py and setup.cfg in python projects - Stack Overflow

最开始是setup.py,然后是setup.cfg,再到现在则为pyproject.toml

打包wheel的命令也有很多种,如build、wheel工具等,示例参考:How to Create Python Packages | Towards Data Science

wheel包安装兼容性问题

安装过程触发本地编译,通过setup.py:

image-20230611121349952

此时如果setup.py过程对平台有依赖,就会报错。

同npm包,wheel包安装失败的原因仅有可能是引入了外部依赖。

兼容性识别关键点:直接跑一遍pip install来检验

java

java调用so的方式是通过JNI或JNA。

jni

JNI用法:Call c function from Java - Stack Overflow

1
2
3
4
5
6
7
8
9
10
11
class HelloWorld {
private native void print(); // 声明c方法

public static void main(String[] args) {
new HelloWorld().print();
}

static {
System.loadLibrary("HelloWorld"); // 加载HelloWorld.so库
}
}

这里还包含怎么编译so库出来的问题,详细见上述链接。

兼容性识别关键点:native、System.loadLibrary关键字

jna

JNA方法:jna/GettingStarted.md at master · java-native-access/jna · GitHub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.sun.jna.examples;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;

/** Simple example of JNA interface mapping and usage. */
public class HelloWorld {

// This is the standard, stable way of mapping, which supports extensive
// customization and mapping of Java to native types.

public interface CLibrary extends Library {
CLibrary INSTANCE = (CLibrary)
Native.load((Platform.isWindows() ? "msvcrt" : "c"),
CLibrary.class);

void printf(String format, Object... args);
}

public static void main(String[] args) {
CLibrary.INSTANCE.printf("Hello, World\n");
for (int i=0;i < args.length;i++) {
CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
}
}

兼容性识别关键点:是否引用了jna依赖。

其他jvm系语言

其他基于java虚拟机开发的语言调用so方法和上述类似,均通过java官方的JNI或JNA方法:

perl

perl语言的特点跟python类似,也是一种脚本解释语言。

代码调用c

perl调用c的方法如下:How do I call a C function from a Perl program? - Stack Overflow

1
2
3
4
5
6
7
8
9
10
use Inline C => <<'__END_OF_C__';

int sum(int a, int b)
{
return (a+b);
}

__END_OF_C__

say sum($x,$y);

ruby

ruby也是一种解释型语言。

代码调用c

ruby调用c的方式是通过rb_define_method方法:How to create a Ruby extension in C in under 5 minutes (rubyinside.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Include the Ruby headers and goodies
#include "ruby.h"

// Defining a space for information and references about the module to be stored internally
VALUE MyTest = Qnil;

// Prototype for the initialization method - Ruby calls this, not you
void Init_mytest();

// Prototype for our method 'test1' - methods are prefixed by 'method_' here
VALUE method_test1(VALUE self);

// The initialization method for this module
void Init_mytest() {
MyTest = rb_define_module("MyTest"); // 定义c模块
rb_define_method(MyTest, "test1", method_test1, 0); // 定义c方法
}

// Our 'test1' method.. it simply returns a value of '10' for now.
VALUE method_test1(VALUE self) {
int x = 10;
return INT2NUM(x);
}

代码调用so

ruby调用so的方式通过借助ffi:Calling C shared libraries with ruby FFI (jertype.com)

1
2
3
4
5
6
7
8
require 'ffi'

module ConcatInterop
extend FFI::Library

ffi_lib './concat.so'
attach_function :concat, [:string, :string, :pointer], :void
end

lua

lua也是一种解释型语言。

代码调用c

lua调用c的方式是通过lua_CFunction方法:How would I make a C-function that I can call from Lua? - Stack Overflow

代码调用so

lua调用so的方式是通过ffi:ffi - How to call a function in shared library using Lua - Stack Overflow

1
2
3
4
5
local lib = ffi.load('some.dll')
ffi.cdef[[
void hello (void);
]]
lib.hello()

php

php想要调用c或so的话,就不是在php内声明c函数签名的方式了,而是通过编写php extension:Is it possible to call C code from php - Stack Overflow

c/c++/fortran/rust/go

这些语言就都是编译型语言了,编译型语言一定是平台相关,意味着不存在出一个包就能达成跨平台的效果,故此类软件一定要做迁移。

对于编译型语言,兼容性主要体现在编译过程,如:

  • 按照默认的readme、官方文档编译步骤,编译过程是否顺畅?
  • 是否存在某个平台相关选项导致编译失败?

评论